WIP: attachments
This commit is contained in:
parent
d84a7266b8
commit
f2bb3a022b
6 changed files with 122 additions and 21 deletions
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in a new issue