WIP: attachments

This commit is contained in:
Philipp Heckel 2022-01-04 00:54:18 +01:00
parent d84a7266b8
commit f2bb3a022b
6 changed files with 122 additions and 21 deletions

View file

@ -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')"
]
}
}

View file

@ -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,
)

View file

@ -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<String>?,
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"

View file

@ -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)

View file

@ -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) {

View file

@ -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)) {