Do not crash preview if icon/attachment too large

This commit is contained in:
Philipp Heckel 2022-09-10 15:24:53 -04:00
parent f6ce3af473
commit f2492904ea
5 changed files with 42 additions and 26 deletions

View file

@ -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)

View file

@ -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."
}
}

View file

@ -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,
@ -475,4 +485,4 @@ fun String.sha256(): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(this.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
}

View file

@ -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)
}
}

View file

@ -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)