Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f2492904 authored by Philipp Heckel's avatar Philipp Heckel
Browse files

Do not crash preview if icon/attachment too large

parent f6ce3af4
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -98,7 +98,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
                    val buffer = ByteArray(BUFFER_SIZE)
                    var bytes = fileIn.read(buffer)
                    while (bytes >= 0) {
                        if (downloadLimit != null && bytesCopied > downloadLimit) {
                        if (bytesCopied > downloadLimit) {
                            throw Exception("Icon is longer than max download size.")
                        }
                        fileOut.write(buffer, 0, bytes)
@@ -106,10 +106,9 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
                        bytes = fileIn.read(buffer)
                    }
                }
                // TODO: Resize icon if >5MB, so it can be previewed. Right now it'll just not be shown.
                Log.d(TAG, "Icon download: successful response, proceeding with download")
                save(icon.copy(
                    contentUri = uri.toString()
                ))
                save(icon.copy(contentUri = uri.toString()))
            }
        } catch (e: Exception) {
            failed(e)
+24 −17
Original line number Diff line number Diff line
@@ -131,13 +131,13 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
                cardView.setCardBackgroundColor(Colors.cardBackgroundColor(context))
            }
            val attachment = notification.attachment
            val attachmentExists = if (attachment?.contentUri != null) fileExists(context, attachment.contentUri) else false
            val iconExists = if (notification.icon?.contentUri != null) fileExists(context, notification.icon.contentUri) else false
            val attachmentFileStat = maybeFileStat(context, attachment?.contentUri)
            val iconFileStat = maybeFileStat(context, notification.icon?.contentUri)
            renderPriority(context, notification)
            resetCardButtons()
            maybeRenderMenu(context, notification, attachmentExists)
            maybeRenderAttachment(context, notification, attachmentExists)
            maybeRenderIcon(context, notification, iconExists)
            maybeRenderMenu(context, notification, attachmentFileStat)
            maybeRenderAttachment(context, notification, attachmentFileStat)
            maybeRenderIcon(context, notification, iconFileStat)
            maybeRenderActions(context, notification)
        }

@@ -165,20 +165,20 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            }
        }

        private fun maybeRenderAttachment(context: Context, notification: Notification, attachmentExists: Boolean) {
        private fun maybeRenderAttachment(context: Context, notification: Notification, attachmentFileStat: FileInfo?) {
            if (notification.attachment == null) {
                attachmentImageView.visibility = View.GONE
                attachmentBoxView.visibility = View.GONE
                return
            }
            val attachment = notification.attachment
            val image = attachment.contentUri != null && attachmentExists && supportedImage(attachment.type)
            val image = attachment.contentUri != null && supportedImage(attachment.type) && previewableImage(attachmentFileStat)
            maybeRenderAttachmentImage(context, attachment, image)
            maybeRenderAttachmentBox(context, notification, attachment, attachmentExists, image)
            maybeRenderAttachmentBox(context, notification, attachment, attachmentFileStat, image)
        }

        private fun maybeRenderIcon(context: Context, notification: Notification, iconExists: Boolean) {
            if (notification.icon == null || !iconExists) {
        private fun maybeRenderIcon(context: Context, notification: Notification, iconStat: FileInfo?) {
            if (notification.icon == null || !previewableImage(iconStat)) {
                iconView.visibility = View.GONE
                return
            }
@@ -192,8 +192,8 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            }
        }

        private fun maybeRenderMenu(context: Context, notification: Notification, attachmentExists: Boolean) {
            val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentExists) // Heavy lifting not during on-click
        private fun maybeRenderMenu(context: Context, notification: Notification, attachmentFileStat: FileInfo?) {
            val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentFileStat) // Heavy lifting not during on-click
            if (menuButtonPopupMenu != null) {
                menuButton.setOnClickListener { menuButtonPopupMenu.show() }
                menuButton.visibility = View.VISIBLE
@@ -238,14 +238,14 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            return button
        }

        private fun maybeRenderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, exists: Boolean, image: Boolean) {
        private fun maybeRenderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, attachmentFileStat: FileInfo?, image: Boolean) {
            if (image) {
                attachmentBoxView.visibility = View.GONE
                return
            }
            attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
            attachmentInfoView.text = formatAttachmentDetails(context, attachment, attachmentFileStat)
            attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
            val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, exists) // Heavy lifting not during on-click
            val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, attachmentFileStat) // Heavy lifting not during on-click
            if (attachmentBoxPopupMenu != null) {
                attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
            } else {
@@ -258,11 +258,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            attachmentBoxView.visibility = View.VISIBLE
        }

        private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, attachmentExists: Boolean): PopupMenu? {
        private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, attachmentFileStat: FileInfo?): PopupMenu? {
            val popup = PopupMenu(context, anchor)
            popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
            val attachment = notification.attachment // May be null
            val hasAttachment = attachment != null
            val attachmentExists = attachmentFileStat != null
            val hasClickLink = notification.click != ""
            val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
            val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
@@ -300,8 +301,9 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            return popup
        }

        private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String {
        private fun formatAttachmentDetails(context: Context, attachment: Attachment, attachmentFileStat: FileInfo?): String {
            val name = attachment.name
            val exists = attachmentFileStat != null
            val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE
            val downloading = !exists && attachment.progress in 0..99
            val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED)
@@ -517,6 +519,10 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
            }
            context.sendBroadcast(intent)
        }

        private fun previewableImage(fileStat: FileInfo?): Boolean {
            return if (fileStat != null) fileStat.size <= IMAGE_PREVIEW_MAX_BYTES else false
        }
    }

    object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
@@ -532,5 +538,6 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
    companion object {
        const val TAG = "NtfyDetailAdapter"
        const val REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD = 9876
        const val IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024 // Too large images crash the app with "Canvas: trying to draw too large(233280000bytes) bitmap."
    }
}
+12 −2
Original line number Diff line number Diff line
@@ -37,7 +37,9 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source
import java.io.*
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.security.MessageDigest
import java.security.SecureRandom
import java.text.DateFormat
@@ -260,6 +262,14 @@ fun fileStat(context: Context, contentUri: Uri?): FileInfo {
    }
}

fun maybeFileStat(context: Context, contentUri: String?): FileInfo? {
    return try {
        fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist
    } catch (_: Exception) {
        null
    }
}

data class FileInfo(
    val filename: String,
    val size: Long,
+1 −3
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
        val activeIconUris = repository.getActiveIconUris()
        val activeIconFilenames = activeIconUris.map{ fileStat(applicationContext, Uri.parse(it)).filename }.toSet()
        val iconDir = File(applicationContext.cacheDir, DownloadIconWorker.ICON_CACHE_DIR)
        val allIconFilenames = iconDir.listFiles().map{ file -> file.name }
        val allIconFilenames = iconDir.listFiles()?.map{ file -> file.name }.orEmpty()
        val filenamesToDelete = allIconFilenames.minus(activeIconFilenames)
        filenamesToDelete.forEach { filename ->
            try {
@@ -80,7 +80,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
                if (!deleted) {
                    Log.w(TAG, "Unable to delete icon: $filename")
                }

                val uri = FileProvider.getUriForFile(applicationContext,
                    DownloadIconWorker.FILE_PROVIDER_AUTHORITY, file).toString()
                repository.clearIconUri(uri)
@@ -115,7 +114,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
            val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS
            Log.d(TAG, "[$logId] Hard deleting notifications older than $markDeletedOlderThanTimestamp")
            repository.removeNotificationsIfOlderThan(subscription.id, deleteOlderThanTimestamp)

        }
    }

+2 −0
Original line number Diff line number Diff line
@@ -4,11 +4,13 @@ Features:
* Polling is now done with since=<id> API, which makes deduping easier (#165)
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
* Move action buttons in notification cards (#236, thanks to @wunter8)
* Icons can be set for each individual notification (#126, thanks to @wunter8)

Bugs:
* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast (#329, thanks to @wunter8)
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label (#292, thanks to @mhameed for reporting)
* Do not crash app if preview image too large (no ticket)

Additional translations:
* Italian (thanks to @Genio2003)