diff --git a/app/schemas/io.heckel.ntfy.data.Database/5.json b/app/schemas/io.heckel.ntfy.data.Database/5.json index 451cc91..2131068 100644 --- a/app/schemas/io.heckel.ntfy.data.Database/5.json +++ b/app/schemas/io.heckel.ntfy.data.Database/5.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 5, - "identityHash": "425a0bc96c8aae9d01985b0f4d7579dc", + "identityHash": "fd7d1e0ac6ac7d68eb79ffe928dae67a", "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, `attachmentName` TEXT, `attachmentType` TEXT, `attachmentExpires` INTEGER, `attachmentUrl` TEXT, `deleted` INTEGER NOT NULL, 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, `attachmentName` TEXT, `attachmentType` TEXT, `attachmentSize` INTEGER, `attachmentExpires` INTEGER, `attachmentPreviewUrl` TEXT, `attachmentUrl` TEXT, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))", "fields": [ { "fieldPath": "id", @@ -143,12 +143,24 @@ "affinity": "TEXT", "notNull": false }, + { + "fieldPath": "attachmentSize", + "columnName": "attachmentSize", + "affinity": "INTEGER", + "notNull": false + }, { "fieldPath": "attachmentExpires", "columnName": "attachmentExpires", "affinity": "INTEGER", "notNull": false }, + { + "fieldPath": "attachmentPreviewUrl", + "columnName": "attachmentPreviewUrl", + "affinity": "TEXT", + "notNull": false + }, { "fieldPath": "attachmentUrl", "columnName": "attachmentUrl", @@ -176,7 +188,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, '425a0bc96c8aae9d01985b0f4d7579dc')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fd7d1e0ac6ac7d68eb79ffe928dae67a')" ] } } \ No newline at end of file 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 deb155b..c5e8b57 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -55,6 +55,7 @@ data class Notification( @ColumnInfo(name = "attachmentType") val attachmentType: String?, // MIME type @ColumnInfo(name = "attachmentSize") val attachmentSize: Long?, // Size in bytes @ColumnInfo(name = "attachmentExpires") val attachmentExpires: Long?, // Unix timestamp + @ColumnInfo(name = "attachmentPreviewUrl") val attachmentPreviewUrl: String?, @ColumnInfo(name = "attachmentUrl") val attachmentUrl: String?, @ColumnInfo(name = "deleted") val deleted: Boolean, ) 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 58dfc99..5bacbed 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -123,7 +123,8 @@ class ApiService { attachmentName = message.attachment?.name, attachmentType = message.attachment?.type, attachmentSize = message.attachment?.size, - attachmentExpires = message.attachment?.expires?.toLong(), + attachmentExpires = message.attachment?.expires, + attachmentPreviewUrl = message.attachment?.preview_url, attachmentUrl = message.attachment?.url, notificationId = Random.nextInt(), deleted = false @@ -158,6 +159,7 @@ class ApiService { attachmentType = message.attachment?.type, attachmentSize = message.attachment?.size, attachmentExpires = message.attachment?.expires, + attachmentPreviewUrl = message.attachment?.preview_url, attachmentUrl = message.attachment?.url, notificationId = 0, deleted = false @@ -182,9 +184,10 @@ class ApiService { @Keep private data class Attachment( val name: String, - val type: String, - val size: Long, - val expires: Long, + 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/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt index c976a64..83e1a8e 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -9,7 +9,9 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.RingtoneManager +import android.net.Uri import android.os.Build +import android.os.Environment import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat @@ -22,8 +24,10 @@ import io.heckel.ntfy.util.formatMessage import io.heckel.ntfy.util.formatTitle import okhttp3.OkHttpClient import okhttp3.Request +import java.io.File import java.util.concurrent.TimeUnit + class NotificationService(val context: Context) { private val client = OkHttpClient.Builder() .callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request @@ -35,11 +39,9 @@ class NotificationService(val context: Context) { fun display(subscription: Subscription, notification: Notification) { Log.d(TAG, "Displaying notification $notification") - val imageAttachment = notification.attachmentUrl != null && (notification.attachmentType?.startsWith("image/") ?: false) - if (imageAttachment) { - downloadImageAndDisplay(subscription, notification) - } else { - displayInternal(subscription, notification) + displayInternal(subscription, notification) + if (notification.attachmentPreviewUrl != null) { + downloadPreviewAndUpdate(subscription, notification) } } @@ -81,8 +83,19 @@ class NotificationService(val context: Context) { .setSound(defaultSoundUri) .setContentIntent(pendingIntent) // Click target for notification .setAutoCancel(true) // Cancel when notification is clicked + if (notification.attachmentUrl != null) { + val viewIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse(notification.attachmentUrl)), 0) + notificationBuilder + .addAction(NotificationCompat.Action.Builder(0, "Open", viewIntent).build()) + .addAction(NotificationCompat.Action.Builder(0, "Copy URL", viewIntent).build()) + .addAction(NotificationCompat.Action.Builder(0, "Download", pendingIntent).build()) + } notificationBuilder = if (bitmap != null) { - notificationBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap)) + notificationBuilder + .setLargeIcon(bitmap) + .setStyle(NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null)) } else { notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) } @@ -92,9 +105,29 @@ class NotificationService(val context: Context) { notificationManager.notify(notification.notificationId, notificationBuilder.build()) } - private fun downloadImageAndDisplay(subscription: Subscription, notification: Notification) { + private fun downloadPreviewAndUpdate(subscription: Subscription, notification: Notification) { + val previewUrl = notification.attachmentPreviewUrl ?: return + Log.d(TAG, "Downloading preview image $previewUrl") + + val request = Request.Builder() + .url(previewUrl) + .addHeader("User-Agent", ApiService.USER_AGENT) + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful || response.body == null) { + Log.d(TAG, "Preview response failed: ${response.code}") + } else { + Log.d(TAG, "Successful response, streaming preview") + val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream()) + displayInternal(subscription, notification, bitmap) + } + } + } + + + private fun downloadPreviewAndUpdateXXX(subscription: Subscription, notification: Notification) { val url = notification.attachmentUrl ?: return - Log.d(TAG, "Downloading image $url") + Log.d(TAG, "Downloading attachment from $url") val request = Request.Builder() .url(url) @@ -102,11 +135,19 @@ class NotificationService(val context: Context) { .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful || response.body == null) { - displayInternal(subscription, notification) - return + Log.d(TAG, "Attachment download failed: ${response.code}") + } else { + Log.d(TAG, "Successful response") + /*val filename = notification.id + val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS + "/ntfy/" + notification.id) + context.openFileOutput(filename, Context.MODE_PRIVATE).use { + response.body!!.byteStream() + }*/ + // TODO work manager + + val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream()) + displayInternal(subscription, notification, bitmap) } - val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream()) - displayInternal(subscription, notification, bitmap) } } 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 ebb97c6..3ebdd9a 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -60,6 +60,7 @@ 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"] if (id == null || topic == null || message == null || timestamp == null) { Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}") @@ -84,6 +85,7 @@ class FirebaseService : FirebaseMessagingService() { attachmentType = attachmentType, attachmentSize = attachmentSize, attachmentExpires = attachmentExpires, + attachmentPreviewUrl = attachmentPreviewUrl, attachmentUrl = attachmentUrl, notificationId = Random.nextInt(), deleted = false