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)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
var bytes = fileIn.read(buffer)
|
var bytes = fileIn.read(buffer)
|
||||||
while (bytes >= 0) {
|
while (bytes >= 0) {
|
||||||
if (downloadLimit != null && bytesCopied > downloadLimit) {
|
if (bytesCopied > downloadLimit) {
|
||||||
throw Exception("Icon is longer than max download size.")
|
throw Exception("Icon is longer than max download size.")
|
||||||
}
|
}
|
||||||
fileOut.write(buffer, 0, bytes)
|
fileOut.write(buffer, 0, bytes)
|
||||||
|
@ -106,10 +106,9 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
|
||||||
bytes = fileIn.read(buffer)
|
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")
|
Log.d(TAG, "Icon download: successful response, proceeding with download")
|
||||||
save(icon.copy(
|
save(icon.copy(contentUri = uri.toString()))
|
||||||
contentUri = uri.toString()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
failed(e)
|
failed(e)
|
||||||
|
|
|
@ -131,13 +131,13 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
cardView.setCardBackgroundColor(Colors.cardBackgroundColor(context))
|
cardView.setCardBackgroundColor(Colors.cardBackgroundColor(context))
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
val attachment = notification.attachment
|
||||||
val attachmentExists = if (attachment?.contentUri != null) fileExists(context, attachment.contentUri) else false
|
val attachmentFileStat = maybeFileStat(context, attachment?.contentUri)
|
||||||
val iconExists = if (notification.icon?.contentUri != null) fileExists(context, notification.icon.contentUri) else false
|
val iconFileStat = maybeFileStat(context, notification.icon?.contentUri)
|
||||||
renderPriority(context, notification)
|
renderPriority(context, notification)
|
||||||
resetCardButtons()
|
resetCardButtons()
|
||||||
maybeRenderMenu(context, notification, attachmentExists)
|
maybeRenderMenu(context, notification, attachmentFileStat)
|
||||||
maybeRenderAttachment(context, notification, attachmentExists)
|
maybeRenderAttachment(context, notification, attachmentFileStat)
|
||||||
maybeRenderIcon(context, notification, iconExists)
|
maybeRenderIcon(context, notification, iconFileStat)
|
||||||
maybeRenderActions(context, notification)
|
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) {
|
if (notification.attachment == null) {
|
||||||
attachmentImageView.visibility = View.GONE
|
attachmentImageView.visibility = View.GONE
|
||||||
attachmentBoxView.visibility = View.GONE
|
attachmentBoxView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
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)
|
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) {
|
private fun maybeRenderIcon(context: Context, notification: Notification, iconStat: FileInfo?) {
|
||||||
if (notification.icon == null || !iconExists) {
|
if (notification.icon == null || !previewableImage(iconStat)) {
|
||||||
iconView.visibility = View.GONE
|
iconView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -192,8 +192,8 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderMenu(context: Context, notification: Notification, attachmentExists: Boolean) {
|
private fun maybeRenderMenu(context: Context, notification: Notification, attachmentFileStat: FileInfo?) {
|
||||||
val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentExists) // Heavy lifting not during on-click
|
val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, attachmentFileStat) // Heavy lifting not during on-click
|
||||||
if (menuButtonPopupMenu != null) {
|
if (menuButtonPopupMenu != null) {
|
||||||
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
||||||
menuButton.visibility = View.VISIBLE
|
menuButton.visibility = View.VISIBLE
|
||||||
|
@ -238,14 +238,14 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
return button
|
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) {
|
if (image) {
|
||||||
attachmentBoxView.visibility = View.GONE
|
attachmentBoxView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
attachmentInfoView.text = formatAttachmentDetails(context, attachment, attachmentFileStat)
|
||||||
attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
|
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) {
|
if (attachmentBoxPopupMenu != null) {
|
||||||
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
||||||
} else {
|
} else {
|
||||||
|
@ -258,11 +258,12 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
attachmentBoxView.visibility = View.VISIBLE
|
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)
|
val popup = PopupMenu(context, anchor)
|
||||||
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||||
val attachment = notification.attachment // May be null
|
val attachment = notification.attachment // May be null
|
||||||
val hasAttachment = attachment != null
|
val hasAttachment = attachment != null
|
||||||
|
val attachmentExists = attachmentFileStat != null
|
||||||
val hasClickLink = notification.click != ""
|
val hasClickLink = notification.click != ""
|
||||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
|
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
|
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 name = attachment.name
|
||||||
|
val exists = attachmentFileStat != null
|
||||||
val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE
|
val notYetDownloaded = !exists && attachment.progress == ATTACHMENT_PROGRESS_NONE
|
||||||
val downloading = !exists && attachment.progress in 0..99
|
val downloading = !exists && attachment.progress in 0..99
|
||||||
val deleted = !exists && (attachment.progress == ATTACHMENT_PROGRESS_DONE || attachment.progress == ATTACHMENT_PROGRESS_DELETED)
|
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)
|
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>() {
|
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {
|
||||||
|
@ -532,5 +538,6 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "NtfyDetailAdapter"
|
const val TAG = "NtfyDetailAdapter"
|
||||||
const val REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD = 9876
|
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 okhttp3.RequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
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.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.DateFormat
|
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(
|
data class FileInfo(
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
|
@ -475,4 +485,4 @@ fun String.sha256(): String {
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
val digest = md.digest(this.toByteArray())
|
val digest = md.digest(this.toByteArray())
|
||||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
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 activeIconUris = repository.getActiveIconUris()
|
||||||
val activeIconFilenames = activeIconUris.map{ fileStat(applicationContext, Uri.parse(it)).filename }.toSet()
|
val activeIconFilenames = activeIconUris.map{ fileStat(applicationContext, Uri.parse(it)).filename }.toSet()
|
||||||
val iconDir = File(applicationContext.cacheDir, DownloadIconWorker.ICON_CACHE_DIR)
|
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)
|
val filenamesToDelete = allIconFilenames.minus(activeIconFilenames)
|
||||||
filenamesToDelete.forEach { filename ->
|
filenamesToDelete.forEach { filename ->
|
||||||
try {
|
try {
|
||||||
|
@ -80,7 +80,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
Log.w(TAG, "Unable to delete icon: $filename")
|
Log.w(TAG, "Unable to delete icon: $filename")
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = FileProvider.getUriForFile(applicationContext,
|
val uri = FileProvider.getUriForFile(applicationContext,
|
||||||
DownloadIconWorker.FILE_PROVIDER_AUTHORITY, file).toString()
|
DownloadIconWorker.FILE_PROVIDER_AUTHORITY, file).toString()
|
||||||
repository.clearIconUri(uri)
|
repository.clearIconUri(uri)
|
||||||
|
@ -115,7 +114,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
|
||||||
val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS
|
val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS
|
||||||
Log.d(TAG, "[$logId] Hard deleting notifications older than $markDeletedOlderThanTimestamp")
|
Log.d(TAG, "[$logId] Hard deleting notifications older than $markDeletedOlderThanTimestamp")
|
||||||
repository.removeNotificationsIfOlderThan(subscription.id, deleteOlderThanTimestamp)
|
repository.removeNotificationsIfOlderThan(subscription.id, deleteOlderThanTimestamp)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,13 @@ Features:
|
||||||
* Polling is now done with since=<id> API, which makes deduping easier (#165)
|
* Polling is now done with since=<id> API, which makes deduping easier (#165)
|
||||||
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
|
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
|
||||||
* Move action buttons in notification cards (#236, thanks to @wunter8)
|
* Move action buttons in notification cards (#236, thanks to @wunter8)
|
||||||
|
* Icons can be set for each individual notification (#126, thanks to @wunter8)
|
||||||
|
|
||||||
Bugs:
|
Bugs:
|
||||||
* Long-click selecting of notifications doesn't scoll to the top anymore (#235, thanks to @wunter8)
|
* 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)
|
* 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)
|
* 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:
|
Additional translations:
|
||||||
* Italian (thanks to @Genio2003)
|
* Italian (thanks to @Genio2003)
|
||||||
|
|
Loading…
Reference in a new issue