diff --git a/app/schemas/io.heckel.ntfy.data.Database/6.json b/app/schemas/io.heckel.ntfy.data.Database/6.json
index c6efc44..f37de76 100644
--- a/app/schemas/io.heckel.ntfy.data.Database/6.json
+++ b/app/schemas/io.heckel.ntfy.data.Database/6.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 6,
- "identityHash": "1ab02dd84a7f2655b4fc651574b24240",
+ "identityHash": "fc725df9153ee7088ae8024428b7f2cf",
"entities": [
{
"tableName": "Subscription",
@@ -80,7 +80,7 @@
},
{
"tableName": "Notification",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_previewUrl` TEXT, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_previewFile` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
"fields": [
{
"fieldPath": "id",
@@ -167,12 +167,6 @@
"affinity": "INTEGER",
"notNull": false
},
- {
- "fieldPath": "attachment.previewUrl",
- "columnName": "attachment_previewUrl",
- "affinity": "TEXT",
- "notNull": false
- },
{
"fieldPath": "attachment.url",
"columnName": "attachment_url",
@@ -185,12 +179,6 @@
"affinity": "TEXT",
"notNull": false
},
- {
- "fieldPath": "attachment.previewFile",
- "columnName": "attachment_previewFile",
- "affinity": "TEXT",
- "notNull": false
- },
{
"fieldPath": "attachment.progress",
"columnName": "attachment_progress",
@@ -212,7 +200,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ab02dd84a7f2655b4fc651574b24240')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fc725df9153ee7088ae8024428b7f2cf')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c9f7771..f869faa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -108,5 +108,6 @@
+
diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt
index 033411e..cd0e20e 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Database.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt
@@ -63,14 +63,12 @@ data class Attachment(
@ColumnInfo(name = "type") val type: String?, // MIME type
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
@ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp
- @ColumnInfo(name = "previewUrl") val previewUrl: String?,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "contentUri") val contentUri: String?,
- @ColumnInfo(name = "previewFile") val previewFile: String?,
@ColumnInfo(name = "progress") val progress: Int,
) {
- constructor(name: String?, type: String?, size: Long?, expires: Long?, previewUrl: String?, url: String) :
- this(name, type, size, expires, previewUrl, url, null, null, 0)
+ constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) :
+ this(name, type, size, expires, url, null, 0)
}
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
index a108222..c5b301f 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt
@@ -7,11 +7,7 @@ import com.google.gson.Gson
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification
-import io.heckel.ntfy.util.topicUrl
-import io.heckel.ntfy.util.topicUrlJson
-import io.heckel.ntfy.util.topicUrlJsonPoll
-import io.heckel.ntfy.util.toPriority
-import io.heckel.ntfy.util.joinTags
+import io.heckel.ntfy.util.*
import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
@@ -119,7 +115,6 @@ class ApiService {
type = message.attachment.type,
size = message.attachment.size,
expires = message.attachment.expires,
- previewUrl = message.attachment.preview_url,
url = message.attachment.url,
)
} else null
@@ -160,7 +155,6 @@ class ApiService {
type = message.attachment.type,
size = message.attachment.size,
expires = message.attachment.expires,
- previewUrl = message.attachment.preview_url,
url = message.attachment.url,
)
} else null
@@ -174,7 +168,7 @@ class ApiService {
tags = joinTags(message.tags),
click = message.click ?: "",
attachment = attachment,
- notificationId = 0, // zero!
+ notificationId = 0, // zero: when we poll, we do not want a notificationId!
deleted = false
)
}
@@ -192,16 +186,15 @@ class ApiService {
val click: String?,
val title: String?,
val message: String,
- val attachment: Attachment?,
+ val attachment: MessageAttachment?,
)
@Keep
- private data class Attachment(
+ private data class MessageAttachment(
val name: String,
val type: String?,
val size: Long?,
val expires: Long?,
- val preview_url: String?,
val url: String,
)
diff --git a/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt
index 86c6fa6..cb5e864 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt
@@ -2,7 +2,6 @@ package io.heckel.ntfy.msg
import android.content.ContentValues
import android.content.Context
-import android.graphics.BitmapFactory
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
@@ -13,10 +12,9 @@ import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
+import io.heckel.ntfy.msg.NotificationService.Companion.PROGRESS_DONE
import okhttp3.OkHttpClient
import okhttp3.Request
-import java.io.File
-import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
class AttachmentDownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
@@ -36,35 +34,10 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
val notification = repository.getNotification(notificationId) ?: return Result.failure()
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
val attachment = notification.attachment ?: return Result.failure()
- if (attachment.previewUrl != null) {
- downloadPreview(subscription, notification, attachment)
- }
downloadAttachment(repository, subscription, notification, attachment)
return Result.success()
}
- private fun downloadPreview(subscription: Subscription, notification: Notification, attachment: Attachment) {
- val url = attachment.previewUrl ?: return
- Log.d(TAG, "Downloading preview from $url")
-
- val request = Request.Builder()
- .url(url)
- .addHeader("User-Agent", ApiService.USER_AGENT)
- .build()
- client.newCall(request).execute().use { response ->
- if (!response.isSuccessful || response.body == null) {
- throw Exception("Preview download failed: ${response.code}")
- }
- val previewFile = File(applicationContext.cacheDir.absolutePath, "preview-" + notification.id)
- Log.d(TAG, "Downloading preview to cache file: $previewFile")
- FileOutputStream(previewFile).use { fileOut ->
- response.body!!.byteStream().copyTo(fileOut)
- }
- Log.d(TAG, "Preview downloaded; updating notification")
- notifier.update(subscription, notification)
- }
- }
-
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) {
Log.d(TAG, "Downloading attachment from ${attachment.url}")
@@ -111,7 +84,7 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
val newAttachment = attachment.copy(contentUri = uri.toString())
val newNotification = notification.copy(attachment = newAttachment)
repository.updateNotification(newNotification)
- notifier.update(subscription, newNotification)
+ notifier.update(subscription, newNotification, progress = PROGRESS_DONE)
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt
index 7a8743d..ebdda36 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt
@@ -42,7 +42,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
}
}
if (download) {
- // Download attachment (+ preview if available) in the background via WorkManager
+ // Download attachment in the background via WorkManager
// The indirection via WorkManager is required since this code may be executed
// in a doze state and Internet may not be available. It's also best practice apparently.
scheduleAttachmentDownload(notification)
diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
index 1f69582..22b708a 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -1,12 +1,7 @@
package io.heckel.ntfy.msg
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.TaskStackBuilder
-import android.content.Context
-import android.content.Intent
-import android.graphics.Bitmap
+import android.app.*
+import android.content.*
import android.graphics.BitmapFactory
import android.media.RingtoneManager
import android.net.Uri
@@ -14,18 +9,15 @@ import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
-import androidx.work.OneTimeWorkRequest
-import androidx.work.WorkManager
-import androidx.work.workDataOf
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
-import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
+import io.heckel.ntfy.util.formatBytes
+import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.formatMessage
import io.heckel.ntfy.util.formatTitle
-import java.io.File
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -54,7 +46,7 @@ class NotificationService(val context: Context) {
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false, progress: Int = PROGRESS_NONE) {
val title = formatTitle(subscription, notification)
- val message = formatMessage(notification)
+ val message = maybeWithAttachmentInfo(formatMessage(notification), notification, progress)
val channelId = toChannelId(notification.priority)
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
@@ -68,12 +60,25 @@ class NotificationService(val context: Context) {
maybeSetSound(builder, update)
maybeSetProgress(builder, progress)
maybeAddOpenAction(builder, notification)
- maybeAddCopyUrlAction(builder, notification)
+ maybeAddBrowseAction(builder, notification)
maybeCreateNotificationChannel(notification.priority)
notificationManager.notify(notification.notificationId, builder.build())
}
+ private fun maybeWithAttachmentInfo(message: String, notification: Notification, progress: Int): String {
+ if (progress < 0 || notification.attachment == null) return message
+ val att = notification.attachment
+ val infos = mutableListOf()
+ 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 (progress >= 0) infos.add("${progress}%")
+ if (infos.size == 0) return message
+ if (progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}"
+ return "${message}\nFile: ${infos.joinToString(", ")}"
+ }
+
private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) {
if (!update) {
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
@@ -84,10 +89,13 @@ class NotificationService(val context: Context) {
}
private fun setStyle(builder: NotificationCompat.Builder, notification: Notification, message: String) {
- val previewFile = File(context.applicationContext.cacheDir.absolutePath, "preview-" + notification.id)
- if (previewFile.exists()) {
+ val contentUri = notification.attachment?.contentUri
+ val isImage = listOf("image/jpeg", "image/png").contains(notification.attachment?.type)
+ if (contentUri != null && isImage) {
try {
- val bitmap = BitmapFactory.decodeFile(previewFile.absolutePath)
+ val resolver = context.applicationContext.contentResolver
+ val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
+ val bitmap = BitmapFactory.decodeStream(bitmapStream)
builder
.setLargeIcon(bitmap)
.setStyle(NotificationCompat.BigPictureStyle()
@@ -116,26 +124,27 @@ class NotificationService(val context: Context) {
}
private fun maybeSetProgress(builder: NotificationCompat.Builder, progress: Int) {
- if (progress >= 0) {
+ if (progress in 0..99) {
builder.setProgress(100, progress, false)
} else {
builder.setProgress(0, 0, false) // Remove progress bar
}
}
- private fun maybeAddOpenAction(notificationBuilder: NotificationCompat.Builder, notification: Notification) {
+ private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachment.contentUri)
- val openIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, contentUri), 0)
- notificationBuilder.addAction(NotificationCompat.Action.Builder(0, "Open", openIntent).build())
+ val intent = Intent(Intent.ACTION_VIEW, contentUri)
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+ builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build())
}
}
- private fun maybeAddCopyUrlAction(builder: NotificationCompat.Builder, notification: Notification) {
- if (notification.attachment?.url != null) {
- // XXXXXXXXx
- val copyUrlIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse(notification.attachment.url)), 0)
- builder.addAction(NotificationCompat.Action.Builder(0, "Copy URL", copyUrlIntent).build())
+ private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) {
+ if (notification.attachment?.contentUri != null) {
+ val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+ builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build())
}
}
@@ -202,8 +211,10 @@ class NotificationService(val context: Context) {
companion object {
const val PROGRESS_NONE = -1
const val PROGRESS_INDETERMINATE = -2
+ const val PROGRESS_DONE = 100
private const val TAG = "NtfyNotifService"
+
private const val CHANNEL_ID_MIN = "ntfy-min"
private const val CHANNEL_ID_LOW = "ntfy-low"
private const val CHANNEL_ID_DEFAULT = "ntfy"
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
index 61a3d36..04a19d1 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -7,6 +7,7 @@ import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import java.security.SecureRandom
import java.text.DateFormat
+import java.text.StringCharacterIterator
import java.util.*
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
@@ -19,8 +20,8 @@ fun topicShortUrl(baseUrl: String, topic: String) =
.replace("https://", "")
fun formatDateShort(timestampSecs: Long): String {
- val mutedUntilDate = Date(timestampSecs*1000)
- return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate)
+ val date = Date(timestampSecs*1000)
+ return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
}
fun toPriority(priority: Int?): Int {
@@ -126,3 +127,20 @@ fun randomString(len: Int): String {
inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
return if (p1 != null && p2 != null) block(p1, p2) else null
}
+
+fun formatBytes(bytes: Long): String {
+ val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes)
+ if (absB < 1024) {
+ return "$bytes B"
+ }
+ var value = absB
+ val ci = StringCharacterIterator("KMGTPE")
+ var i = 40
+ while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
+ value = value shr 10
+ ci.next()
+ i -= 10
+ }
+ value *= java.lang.Long.signum(bytes).toLong()
+ return java.lang.String.format("%.1f %ciB", value / 1024.0, ci.current())
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1767b78..f3459ed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -144,6 +144,10 @@
Until tomorrow
Forever
+
+ Open
+ Browse
+
Settings
Notifications
diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
index 2bd5142..b7f062b 100644
--- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
+++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt
@@ -62,7 +62,6 @@ class FirebaseService : FirebaseMessagingService() {
val attachmentType = data["attachment_type"]
val attachmentSize = data["attachment_size"]?.toLongOrNull()
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()
- val attachmentPreviewUrl = data["attachment_preview_url"]
val attachmentUrl = data["attachment_url"]
val truncated = (data["truncated"] ?: "") == "1"
if (id == null || topic == null || message == null || timestamp == null) {
@@ -88,7 +87,6 @@ class FirebaseService : FirebaseMessagingService() {
type = attachmentType,
size = attachmentSize,
expires = attachmentExpires,
- previewUrl = attachmentPreviewUrl,
url = attachmentUrl,
)
} else null