diff --git a/app/schemas/io.heckel.ntfy.data.Database/5.json b/app/schemas/io.heckel.ntfy.data.Database/5.json index dd398a4..451cc91 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": "306578182c2ad0f9803956beda094d28", + "identityHash": "425a0bc96c8aae9d01985b0f4d7579dc", "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, `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, `attachmentExpires` INTEGER, `attachmentUrl` TEXT, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))", "fields": [ { "fieldPath": "id", @@ -131,6 +131,30 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "attachmentName", + "columnName": "attachmentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentType", + "columnName": "attachmentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentExpires", + "columnName": "attachmentExpires", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachmentUrl", + "columnName": "attachmentUrl", + "affinity": "TEXT", + "notNull": false + }, { "fieldPath": "deleted", "columnName": "deleted", @@ -152,7 +176,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, '306578182c2ad0f9803956beda094d28')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '425a0bc96c8aae9d01985b0f4d7579dc')" ] } } \ 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 578663f..deb155b 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -51,6 +51,11 @@ data class Notification( @ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID @ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max @ColumnInfo(name = "tags") val tags: String, + @ColumnInfo(name = "attachmentName") val attachmentName: String?, // Filename + @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 = "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 afcb3d8..58dfc99 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -120,6 +120,11 @@ class ApiService { message = message.message, priority = toPriority(message.priority), tags = joinTags(message.tags), + attachmentName = message.attachment?.name, + attachmentType = message.attachment?.type, + attachmentSize = message.attachment?.size, + attachmentExpires = message.attachment?.expires?.toLong(), + attachmentUrl = message.attachment?.url, notificationId = Random.nextInt(), deleted = false ) @@ -149,6 +154,11 @@ class ApiService { message = message.message, priority = toPriority(message.priority), tags = joinTags(message.tags), + attachmentName = message.attachment?.name, + attachmentType = message.attachment?.type, + attachmentSize = message.attachment?.size, + attachmentExpires = message.attachment?.expires, + attachmentUrl = message.attachment?.url, notificationId = 0, deleted = false ) @@ -165,12 +175,22 @@ class ApiService { val priority: Int?, val tags: List?, val title: String?, - val message: String + val message: String, + val attachment: Attachment?, + ) + + @Keep + private data class Attachment( + val name: String, + val type: String, + val size: Long, + val expires: Long, + val url: String, ) companion object { + val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" private const val TAG = "NtfyApiService" - private val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" // These constants have corresponding values in the server codebase! const val CONTROL_TOPIC = "~control" 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 c4cec51..9c754d1 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -26,7 +26,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { val broadcast = shouldBroadcast(subscription) val distribute = shouldDistribute(subscription) if (notify) { - notifier.send(subscription, notification) + notifier.display(subscription, notification) } if (broadcast) { broadcaster.send(subscription, notification, muted) 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 a514768..c976a64 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -6,10 +6,11 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.media.RingtoneManager import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import io.heckel.ntfy.R @@ -19,11 +20,43 @@ import io.heckel.ntfy.ui.DetailActivity import io.heckel.ntfy.ui.MainActivity import io.heckel.ntfy.util.formatMessage import io.heckel.ntfy.util.formatTitle +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.concurrent.TimeUnit class NotificationService(val context: Context) { - fun send(subscription: Subscription, notification: Notification) { + private val client = OkHttpClient.Builder() + .callTimeout(15, TimeUnit.SECONDS) // Total timeout for entire request + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build() + + 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) + } + } + + fun cancel(notification: Notification) { + if (notification.notificationId != 0) { + Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}") + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notification.notificationId) + } + } + + fun createNotificationChannels() { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + (1..5).forEach { priority -> maybeCreateNotificationChannel(notificationManager, priority) } + } + + private fun displayInternal(subscription: Subscription, notification: Notification, bitmap: Bitmap? = null) { // Create an Intent for the activity you want to start val intent = Intent(context, DetailActivity::class.java) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) @@ -40,32 +73,41 @@ class NotificationService(val context: Context) { val message = formatMessage(notification) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val channelId = toChannelId(notification.priority) - val notificationBuilder = NotificationCompat.Builder(context, channelId) + var notificationBuilder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) .setColor(ContextCompat.getColor(context, R.color.primaryColor)) .setContentTitle(title) .setContentText(message) - .setStyle(NotificationCompat.BigTextStyle().bigText(message)) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) // Click target for notification .setAutoCancel(true) // Cancel when notification is clicked + notificationBuilder = if (bitmap != null) { + notificationBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap)) + } else { + notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) + } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager maybeCreateNotificationChannel(notificationManager, notification.priority) notificationManager.notify(notification.notificationId, notificationBuilder.build()) } - fun cancel(notification: Notification) { - if (notification.notificationId != 0) { - Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}") - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(notification.notificationId) - } - } + private fun downloadImageAndDisplay(subscription: Subscription, notification: Notification) { + val url = notification.attachmentUrl ?: return + Log.d(TAG, "Downloading image $url") - fun createNotificationChannels() { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - (1..5).forEach { priority -> maybeCreateNotificationChannel(notificationManager, priority) } + 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) { + displayInternal(subscription, notification) + return + } + val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream()) + displayInternal(subscription, notification, bitmap) + } } private fun maybeCreateNotificationChannel(notificationManager: NotificationManager, priority: Int) { 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 a06ea2c..ebb97c6 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -56,6 +56,11 @@ class FirebaseService : FirebaseMessagingService() { val message = data["message"] val priority = data["priority"]?.toIntOrNull() val tags = data["tags"] + val attachmentName = data["attachment_name"] + val attachmentType = data["attachment_type"] + val attachmentSize = data["attachment_size"]?.toLongOrNull() + val attachmentExpires = data["attachment_expires"]?.toLongOrNull() + 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}") return @@ -73,9 +78,14 @@ class FirebaseService : FirebaseMessagingService() { timestamp = timestamp, title = title ?: "", message = message, - notificationId = Random.nextInt(), priority = toPriority(priority), tags = tags ?: "", + attachmentName = attachmentName, + attachmentType = attachmentType, + attachmentSize = attachmentSize, + attachmentExpires = attachmentExpires, + attachmentUrl = attachmentUrl, + notificationId = Random.nextInt(), deleted = false ) if (repository.addNotification(notification)) {