Do not crash preview if icon/attachment too large
This commit is contained in:
parent
f6ce3af473
commit
f2492904ea
5 changed files with 42 additions and 26 deletions
|
@ -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)
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue