Continued work on attachments
This commit is contained in:
parent
e88f87390e
commit
64612dc47f
15 changed files with 273 additions and 118 deletions
|
@ -59,15 +59,15 @@ data class Notification(
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
@ColumnInfo(name = "name") val name: String?, // Filename
|
@ColumnInfo(name = "name") val name: String, // Filename (mandatory, see ntfy server)
|
||||||
@ColumnInfo(name = "type") val type: String?, // MIME type
|
@ColumnInfo(name = "type") val type: String?, // MIME type
|
||||||
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
|
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
|
||||||
@ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp
|
@ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp
|
||||||
@ColumnInfo(name = "url") val url: String,
|
@ColumnInfo(name = "url") val url: String, // URL (mandatory, see ntfy server)
|
||||||
@ColumnInfo(name = "contentUri") val contentUri: String?,
|
@ColumnInfo(name = "contentUri") val contentUri: String?, // After it's downloaded, the content:// location
|
||||||
@ColumnInfo(name = "progress") val progress: Int,
|
@ColumnInfo(name = "progress") val progress: Int, // Progress during download, -1 if not downloaded
|
||||||
) {
|
) {
|
||||||
constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) :
|
constructor(name: String, type: String?, size: Long?, expires: Long?, url: String) :
|
||||||
this(name, type, size, expires, url, null, PROGRESS_NONE)
|
this(name, type, size, expires, url, null, PROGRESS_NONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,23 +45,24 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
||||||
if (!response.isSuccessful || response.body == null) {
|
if (!response.isSuccessful || response.body == null) {
|
||||||
throw Exception("Attachment download failed: ${response.code}")
|
throw Exception("Attachment download failed: ${response.code}")
|
||||||
}
|
}
|
||||||
val name = attachment.name ?: "attachment.bin"
|
val name = attachment.name
|
||||||
val mimeType = attachment.type ?: "application/octet-stream"
|
|
||||||
val size = attachment.size ?: 0
|
val size = attachment.size ?: 0
|
||||||
val resolver = applicationContext.contentResolver
|
val resolver = applicationContext.contentResolver
|
||||||
val details = ContentValues().apply {
|
val details = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
if (attachment.type != null) {
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, attachment.type)
|
||||||
|
}
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
put(MediaStore.MediaColumns.IS_DOWNLOAD, 1)
|
put(MediaStore.MediaColumns.IS_DOWNLOAD, 1)
|
||||||
}
|
}
|
||||||
val uri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), details)
|
val uri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), details)
|
||||||
?: throw Exception("Cannot get content URI")
|
?: throw Exception("Cannot get content URI")
|
||||||
Log.d(TAG, "Starting download to content URI: $uri")
|
Log.d(TAG, "Starting download to content URI: $uri")
|
||||||
|
var bytesCopied: Long = 0
|
||||||
val out = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
val out = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
||||||
out.use { fileOut ->
|
out.use { fileOut ->
|
||||||
val fileIn = response.body!!.byteStream()
|
val fileIn = response.body!!.byteStream()
|
||||||
var bytesCopied: Long = 0
|
|
||||||
val buffer = ByteArray(8 * 1024)
|
val buffer = ByteArray(8 * 1024)
|
||||||
var bytes = fileIn.read(buffer)
|
var bytes = fileIn.read(buffer)
|
||||||
var lastProgress = 0L
|
var lastProgress = 0L
|
||||||
|
@ -80,7 +81,7 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
||||||
val newAttachment = attachment.copy(contentUri = uri.toString(), progress = PROGRESS_DONE)
|
val newAttachment = attachment.copy(contentUri = uri.toString(), size = bytesCopied, progress = PROGRESS_DONE)
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
val newNotification = notification.copy(attachment = newAttachment)
|
||||||
repository.updateNotification(newNotification)
|
repository.updateNotification(newNotification)
|
||||||
notifier.update(subscription, newNotification)
|
notifier.update(subscription, newNotification)
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.ClipData
|
import android.content.*
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -20,11 +18,14 @@ import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.data.Attachment
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
|
import io.heckel.ntfy.data.PROGRESS_DONE
|
||||||
|
import io.heckel.ntfy.data.PROGRESS_NONE
|
||||||
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.exp
|
||||||
|
|
||||||
|
|
||||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||||
|
@ -59,11 +60,13 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text)
|
||||||
private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text)
|
private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text)
|
||||||
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text)
|
||||||
private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
private val newDotImageView: View = itemView.findViewById(R.id.detail_item_new_dot)
|
||||||
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
|
||||||
private val imageView: ImageView = itemView.findViewById(R.id.detail_item_image)
|
|
||||||
private val attachmentView: TextView = itemView.findViewById(R.id.detail_item_attachment_text)
|
|
||||||
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
|
||||||
|
private val attachmentImageView: ImageView = itemView.findViewById(R.id.detail_item_attachment_image)
|
||||||
|
private val attachmentBoxView: View = itemView.findViewById(R.id.detail_item_attachment_box)
|
||||||
|
private val attachmentIconView: ImageView = itemView.findViewById(R.id.detail_item_attachment_icon)
|
||||||
|
private val attachmentInfoView: TextView = itemView.findViewById(R.id.detail_item_attachment_info)
|
||||||
|
|
||||||
fun bind(notification: Notification) {
|
fun bind(notification: Notification) {
|
||||||
this.notification = notification
|
this.notification = notification
|
||||||
|
@ -73,7 +76,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
|
|
||||||
dateView.text = Date(notification.timestamp * 1000).toString()
|
dateView.text = Date(notification.timestamp * 1000).toString()
|
||||||
messageView.text = formatMessage(notification)
|
messageView.text = formatMessage(notification)
|
||||||
newImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
newDotImageView.visibility = if (notification.notificationId == 0) View.GONE else View.VISIBLE
|
||||||
itemView.setOnClickListener { onClick(notification) }
|
itemView.setOnClickListener { onClick(notification) }
|
||||||
itemView.setOnLongClickListener { onLongClick(notification); true }
|
itemView.setOnLongClickListener { onLongClick(notification); true }
|
||||||
if (notification.title != "") {
|
if (notification.title != "") {
|
||||||
|
@ -91,6 +94,11 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
if (selected.contains(notification.id)) {
|
if (selected.contains(notification.id)) {
|
||||||
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
|
itemView.setBackgroundResource(R.color.primarySelectedRowColor);
|
||||||
}
|
}
|
||||||
|
renderPriority(context, notification)
|
||||||
|
maybeRenderAttachment(context, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderPriority(context: Context, notification: Notification) {
|
||||||
when (notification.priority) {
|
when (notification.priority) {
|
||||||
1 -> {
|
1 -> {
|
||||||
priorityImageView.visibility = View.VISIBLE
|
priorityImageView.visibility = View.VISIBLE
|
||||||
|
@ -112,66 +120,170 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp))
|
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val contentUri = notification.attachment?.contentUri
|
}
|
||||||
val fileExists = if (contentUri != null) fileExists(context, contentUri) else false
|
|
||||||
if (contentUri != null && fileExists && supportedImage(notification.attachment.type)) {
|
|
||||||
try {
|
|
||||||
val resolver = context.applicationContext.contentResolver
|
|
||||||
val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
|
|
||||||
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
|
||||||
imageView.setImageBitmap(bitmap)
|
|
||||||
imageView.visibility = View.VISIBLE
|
|
||||||
} catch (_: Exception) {
|
|
||||||
imageView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
imageView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (notification.attachment != null) {
|
|
||||||
attachmentView.text = formatAttachmentInfo(notification, fileExists)
|
|
||||||
attachmentView.visibility = View.VISIBLE
|
|
||||||
menuButton.visibility = View.VISIBLE
|
|
||||||
menuButton.setOnClickListener { menuView ->
|
|
||||||
val popup = PopupMenu(context, menuView)
|
|
||||||
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
|
||||||
|
|
||||||
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
private fun maybeRenderAttachment(context: Context, notification: Notification) {
|
||||||
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
if (notification.attachment == null) {
|
||||||
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
|
||||||
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
|
||||||
if (contentUri != null && fileExists) {
|
|
||||||
openItem.setOnMenuItemClickListener {
|
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(contentUri))) // FIXME try/catch
|
|
||||||
true
|
|
||||||
}
|
|
||||||
browseItem.setOnMenuItemClickListener {
|
|
||||||
context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
copyUrlItem.setOnMenuItemClickListener {
|
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("attachment url", notification.attachment.url)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast
|
|
||||||
.makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
downloadItem.isVisible = false
|
|
||||||
} else {
|
|
||||||
openItem.isVisible = false
|
|
||||||
browseItem.isVisible = false
|
|
||||||
downloadItem.setOnMenuItemClickListener {
|
|
||||||
scheduleAttachmentDownload(context, notification)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
attachmentView.visibility = View.GONE
|
|
||||||
menuButton.visibility = View.GONE
|
menuButton.visibility = View.GONE
|
||||||
|
attachmentImageView.visibility = View.GONE
|
||||||
|
attachmentBoxView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val attachment = notification.attachment
|
||||||
|
val exists = if (attachment.contentUri != null) fileExists(context, attachment.contentUri) else false
|
||||||
|
maybeRenderAttachmentImage(context, attachment, exists)
|
||||||
|
renderAttachmentBox(context, notification, attachment, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, exists: Boolean) {
|
||||||
|
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
||||||
|
attachmentIconView.setImageResource(if (attachment.type?.startsWith("image/") == true) {
|
||||||
|
R.drawable.ic_file_image_gray_24dp
|
||||||
|
} else if (attachment.type?.startsWith("video/") == true) {
|
||||||
|
R.drawable.ic_file_video_gray_24dp
|
||||||
|
} else if (attachment.type?.startsWith("audio/") == true) {
|
||||||
|
R.drawable.ic_file_audio_gray_24dp
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_file_document_gray_24dp
|
||||||
|
})
|
||||||
|
val menuButtonPopupMenu = createAttachmentPopup(context, menuButton, notification, attachment, exists) // Heavy lifting not during on-click
|
||||||
|
if (menuButtonPopupMenu != null) {
|
||||||
|
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
||||||
|
menuButton.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
menuButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click
|
||||||
|
if (attachmentBoxPopupMenu != null) {
|
||||||
|
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
||||||
|
} else {
|
||||||
|
attachmentBoxView.setOnClickListener {
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_item_cannot_download), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentBoxView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAttachmentPopup(context: Context, anchor: View?, notification: Notification, attachment: Attachment, exists: Boolean): PopupMenu? {
|
||||||
|
val popup = PopupMenu(context, anchor)
|
||||||
|
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
|
||||||
|
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
|
||||||
|
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
|
||||||
|
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
|
||||||
|
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
|
||||||
|
val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000
|
||||||
|
if (attachment.contentUri != null) {
|
||||||
|
openItem.setOnMenuItemClickListener {
|
||||||
|
try {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(attachment.contentUri)))
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_item_cannot_open), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// URI parse exception and others; we don't care!
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
browseItem.setOnMenuItemClickListener {
|
||||||
|
context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
copyUrlItem.setOnMenuItemClickListener {
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("attachment url", attachment.url)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
Toast
|
||||||
|
.makeText(context, context.getString(R.string.detail_item_menu_copy_url_copied), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
downloadItem.setOnMenuItemClickListener {
|
||||||
|
scheduleAttachmentDownload(context, notification)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
openItem.isVisible = exists
|
||||||
|
browseItem.isVisible = exists
|
||||||
|
downloadItem.isVisible = !exists && !expired
|
||||||
|
copyUrlItem.isVisible = !expired
|
||||||
|
val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible && !copyUrlItem.isVisible
|
||||||
|
if (noOptions) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return popup
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String {
|
||||||
|
val name = queryAttachmentFilename(context, attachment)
|
||||||
|
val notYetDownloaded = !exists && attachment.progress == PROGRESS_NONE
|
||||||
|
val downloading = !exists && attachment.progress in 0..99
|
||||||
|
val deleted = !exists && attachment.progress == PROGRESS_DONE
|
||||||
|
val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000
|
||||||
|
val expires = attachment.expires != null && attachment.expires > System.currentTimeMillis()/1000
|
||||||
|
val infos = mutableListOf<String>()
|
||||||
|
if (attachment.size != null) {
|
||||||
|
infos.add(formatBytes(attachment.size))
|
||||||
|
}
|
||||||
|
if (notYetDownloaded) {
|
||||||
|
if (expired) {
|
||||||
|
infos.add("not downloaded, link expired")
|
||||||
|
} else if (expires) {
|
||||||
|
infos.add("not downloaded, link expires ${formatDateShort(attachment.expires!!)}")
|
||||||
|
} else {
|
||||||
|
infos.add("not downloaded")
|
||||||
|
}
|
||||||
|
} else if (downloading) {
|
||||||
|
infos.add("${attachment.progress}% downloaded")
|
||||||
|
} else if (deleted) {
|
||||||
|
if (expired) {
|
||||||
|
infos.add("deleted, link expired")
|
||||||
|
} else if (expires) {
|
||||||
|
infos.add("deleted, link expires ${formatDateShort(attachment.expires!!)}")
|
||||||
|
} else {
|
||||||
|
infos.add("deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (infos.size > 0) {
|
||||||
|
"$name\n${infos.joinToString(", ")}"
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryAttachmentFilename(context: Context, attachment: Attachment): String {
|
||||||
|
if (attachment.contentUri == null) {
|
||||||
|
return attachment.name
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
val cursor = resolver.query(Uri.parse(attachment.contentUri), null, null, null, null) ?: return attachment.name
|
||||||
|
return cursor.use { c ->
|
||||||
|
val nameIndex = c.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
||||||
|
c.moveToFirst()
|
||||||
|
c.getString(nameIndex)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return attachment.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeRenderAttachmentImage(context: Context, att: Attachment, exists: Boolean) {
|
||||||
|
val fileIsImage = att.contentUri != null && exists && supportedImage(att.type)
|
||||||
|
if (!fileIsImage) {
|
||||||
|
attachmentImageView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
val bitmapStream = resolver.openInputStream(Uri.parse(att.contentUri))
|
||||||
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
|
attachmentImageView.setImageBitmap(bitmap)
|
||||||
|
attachmentImageView.visibility = View.VISIBLE
|
||||||
|
} catch (_: Exception) {
|
||||||
|
attachmentImageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,24 +109,6 @@ fun formatTitle(notification: Notification): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME duplicate code
|
|
||||||
fun formatAttachmentInfo(notification: Notification, fileExists: Boolean): String {
|
|
||||||
if (notification.attachment == null) return ""
|
|
||||||
val att = notification.attachment
|
|
||||||
val infos = mutableListOf<String>()
|
|
||||||
if (att.name != null) infos.add(att.name)
|
|
||||||
if (att.size != null) infos.add(formatBytes(att.size))
|
|
||||||
//if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires))
|
|
||||||
if (att.progress in 0..99) infos.add("${att.progress}%")
|
|
||||||
if (!fileExists) {
|
|
||||||
if (att.progress == PROGRESS_NONE) infos.add("not downloaded")
|
|
||||||
else infos.add("deleted")
|
|
||||||
}
|
|
||||||
if (infos.size == 0) return ""
|
|
||||||
if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}"
|
|
||||||
return "\uD83D\uDCC4 " + infos.joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
||||||
fun fileExists(context: Context, uri: String): Boolean {
|
fun fileExists(context: Context, uri: String): Boolean {
|
||||||
val resolver = context.applicationContext.contentResolver
|
val resolver = context.applicationContext.contentResolver
|
||||||
|
@ -175,7 +157,7 @@ fun formatBytes(bytes: Long): String {
|
||||||
i -= 10
|
i -= 10
|
||||||
}
|
}
|
||||||
value *= java.lang.Long.signum(bytes).toLong()
|
value *= java.lang.Long.signum(bytes).toLong()
|
||||||
return java.lang.String.format("%.1f %ciB", value / 1024.0, ci.current())
|
return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun supportedImage(mimeType: String?): Boolean {
|
fun supportedImage(mimeType: String?): Boolean {
|
||||||
|
|
9
app/src/main/res/drawable/ic_file_audio_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_audio_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM6,20V4h7v5h5v11H6zM16,11h-4v3.88c-0.36,-0.24 -0.79,-0.38 -1.25,-0.38c-1.24,0 -2.25,1.01 -2.25,2.25c0,1.24 1.01,2.25 2.25,2.25S13,17.99 13,16.75V13h3V11z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_file_document_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_document_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M8,16h8v2L8,18zM8,12h8v2L8,14zM14,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM18,20L6,20L6,4h7v5h5v11z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_file_image_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_image_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_file_video_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_video_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM6,20V4h7v5h5v11H6zM14,14l2,-1.06v4.12L14,16v1c0,0.55 -0.45,1 -1,1H9c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1V14z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
|
@ -48,7 +48,7 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_title_text"
|
||||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_image"/>
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_image"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="This is an optional title. It can also be a little longer but not too long."
|
android:text="This is an optional title. It can also be a little longer but not too long."
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -71,25 +71,13 @@
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
||||||
android:id="@+id/detail_item_image" app:layout_constraintStart_toStartOf="parent"
|
android:id="@+id/detail_item_attachment_image" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/detail_item_message_text"
|
||||||
android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"
|
android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_text" android:layout_marginBottom="5dp"
|
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"
|
||||||
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"/>
|
android:layout_marginBottom="3dp" app:layout_constraintBottom_toTopOf="@id/detail_item_tags_text"/>
|
||||||
<TextView
|
|
||||||
android:text="📄attachment.jpg, 20.1 KB"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_item_attachment_text"
|
|
||||||
android:textColor="@color/primaryTextColor"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
|
||||||
android:autoLink="web"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_image"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/detail_item_tags_text" android:layout_marginTop="3dp"/>
|
|
||||||
<TextView
|
<TextView
|
||||||
android:text="Tags: ssh, zfs"
|
android:text="Tags: ssh, zfs"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -98,13 +86,43 @@
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_text"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom"/>
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_box"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginBottom="3dp"/>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
|
||||||
|
android:id="@+id/detail_item_attachment_box" app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom"
|
||||||
|
android:visibility="visible" android:layout_marginTop="2dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true" android:focusable="true" android:padding="4dp" android:paddingStart="0dp">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
||||||
|
android:id="@+id/detail_item_attachment_icon" app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/detail_item_attachment_info" android:layout_marginEnd="3dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
/>
|
||||||
|
<TextView
|
||||||
|
android:text="attachment.jpg\n58 MB, not downloaded, expires 1/2/2022 10:30 PM"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/detail_item_attachment_info"
|
||||||
|
android:textColor="@color/primaryTextColor"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/detail_item_attachment_icon"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/detail_item_attachment_icon"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/detail_item_attachment_icon"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="10dp" android:id="@+id/detail_item_padding_bottom"
|
android:layout_height="5dp" android:id="@+id/detail_item_padding_bottom"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/detail_item_tags_text"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_box"/>
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,8 @@
|
||||||
<string name="detail_item_menu_download">Download file</string>
|
<string name="detail_item_menu_download">Download file</string>
|
||||||
<string name="detail_item_menu_copy_url">Copy URL</string>
|
<string name="detail_item_menu_copy_url">Copy URL</string>
|
||||||
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
|
||||||
|
<string name="detail_item_cannot_download">Cannot open or download attachment. Link expired and no local file found.</string>
|
||||||
|
<string name="detail_item_cannot_open">Cannot open attachment: File may have been deleted, or there is no app to open the file.</string>
|
||||||
|
|
||||||
<!-- Detail activity: Action bar -->
|
<!-- Detail activity: Action bar -->
|
||||||
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
<string name="detail_menu_notifications_enabled">Notifications enabled</string>
|
||||||
|
|
|
@ -59,7 +59,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
val priority = data["priority"]?.toIntOrNull()
|
val priority = data["priority"]?.toIntOrNull()
|
||||||
val tags = data["tags"]
|
val tags = data["tags"]
|
||||||
val click = data["click"]
|
val click = data["click"]
|
||||||
val attachmentName = data["attachment_name"]
|
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
||||||
val attachmentType = data["attachment_type"]
|
val attachmentType = data["attachment_type"]
|
||||||
val attachmentSize = data["attachment_size"]?.toLongOrNull()
|
val attachmentSize = data["attachment_size"]?.toLongOrNull()
|
||||||
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()
|
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()
|
||||||
|
|
1
assets/audio_file_black_24dp.svg
Normal file
1
assets/audio_file_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,2H6C4.9,2,4,2.9,4,4v16c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V8L14,2z M6,20V4h7v5h5v11H6z M16,11h-4v3.88 c-0.36-0.24-0.79-0.38-1.25-0.38c-1.24,0-2.25,1.01-2.25,2.25c0,1.24,1.01,2.25,2.25,2.25S13,17.99,13,16.75V13h3V11z"/></g></svg>
|
After Width: | Height: | Size: 428 B |
1
assets/description_black_24dp.svg
Normal file
1
assets/description_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
After Width: | Height: | Size: 271 B |
1
assets/image_black_24dp.svg
Normal file
1
assets/image_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>
|
After Width: | Height: | Size: 296 B |
1
assets/video_file_black_24dp.svg
Normal file
1
assets/video_file_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,2H6C4.9,2,4,2.9,4,4v16c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V8L14,2z M6,20V4h7v5h5v11H6z M14,14l2-1.06v4.12L14,16v1 c0,0.55-0.45,1-1,1H9c-0.55,0-1-0.45-1-1v-4c0-0.55,0.45-1,1-1h4c0.55,0,1,0.45,1,1V14z"/></g></svg>
|
After Width: | Height: | Size: 411 B |
Loading…
Reference in a new issue