Schedule attachment download in dispatcher, not in NotificationService

This commit is contained in:
Philipp Heckel 2022-01-08 15:49:07 -05:00
parent be750b603b
commit d440d0a633
7 changed files with 161 additions and 112 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 6, "version": 6,
"identityHash": "6fd36c6995d3ae734f4ba7c8beaf9a95", "identityHash": "1ab02dd84a7f2655b4fc651574b24240",
"entities": [ "entities": [
{ {
"tableName": "Subscription", "tableName": "Subscription",
@ -80,7 +80,7 @@
}, },
{ {
"tableName": "Notification", "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, `attachmentName` TEXT, `attachmentType` TEXT, `attachmentSize` INTEGER, `attachmentExpires` INTEGER, `attachmentPreviewUrl` TEXT, `attachmentUrl` TEXT, `attachmentContentUri` 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, `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`))",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -137,53 +137,65 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{
"fieldPath": "attachmentName",
"columnName": "attachmentName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachmentType",
"columnName": "attachmentType",
"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",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachmentContentUri",
"columnName": "attachmentContentUri",
"affinity": "TEXT",
"notNull": false
},
{ {
"fieldPath": "deleted", "fieldPath": "deleted",
"columnName": "deleted", "columnName": "deleted",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
},
{
"fieldPath": "attachment.name",
"columnName": "attachment_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.type",
"columnName": "attachment_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.size",
"columnName": "attachment_size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachment.expires",
"columnName": "attachment_expires",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachment.previewUrl",
"columnName": "attachment_previewUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.url",
"columnName": "attachment_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.contentUri",
"columnName": "attachment_contentUri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.previewFile",
"columnName": "attachment_previewFile",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "attachment.progress",
"columnName": "attachment_progress",
"affinity": "INTEGER",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -200,7 +212,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, '6fd36c6995d3ae734f4ba7c8beaf9a95')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ab02dd84a7f2655b4fc651574b24240')"
] ]
} }
} }

View file

@ -15,6 +15,7 @@ data class Subscription(
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule @ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name @ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
// TODO autoDownloadAttachments, minPriority
@Ignore val totalCount: Int = 0, // Total notifications @Ignore val totalCount: Int = 0, // Total notifications
@Ignore val newCount: Int = 0, // New notifications @Ignore val newCount: Int = 0, // New notifications
@Ignore val lastActive: Long = 0, // Unix timestamp @Ignore val lastActive: Long = 0, // Unix timestamp
@ -52,16 +53,26 @@ data class Notification(
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max @ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
@ColumnInfo(name = "tags") val tags: String, @ColumnInfo(name = "tags") val tags: String,
@ColumnInfo(name = "click") val click: String, // URL/intent to open on notification click @ColumnInfo(name = "click") val click: String, // URL/intent to open on notification click
@ColumnInfo(name = "attachmentName") val attachmentName: String?, // Filename @Embedded(prefix = "attachment_") val attachment: Attachment?,
@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 = "attachmentContentUri") val attachmentContentUri: String?,
@ColumnInfo(name = "deleted") val deleted: Boolean, @ColumnInfo(name = "deleted") val deleted: Boolean,
) )
@Entity
data class Attachment(
@ColumnInfo(name = "name") val name: String?, // Filename
@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)
}
@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6) @androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6)
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun subscriptionDao(): SubscriptionDao abstract fun subscriptionDao(): SubscriptionDao

View file

@ -5,6 +5,7 @@ import android.util.Log
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.Gson import com.google.gson.Gson
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.util.topicUrl import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.util.topicUrlJson import io.heckel.ntfy.util.topicUrlJson
@ -112,6 +113,16 @@ class ApiService {
val message = gson.fromJson(line, Message::class.java) val message = gson.fromJson(line, Message::class.java)
if (message.event == EVENT_MESSAGE) { if (message.event == EVENT_MESSAGE) {
val topic = message.topic val topic = message.topic
val attachment = if (message.attachment?.url != null) {
Attachment(
name = message.attachment.name,
type = message.attachment.type,
size = message.attachment.size,
expires = message.attachment.expires,
previewUrl = message.attachment.preview_url,
url = message.attachment.url,
)
} else null
val notification = Notification( val notification = Notification(
id = message.id, id = message.id,
subscriptionId = 0, // TO BE SET downstream subscriptionId = 0, // TO BE SET downstream
@ -121,13 +132,7 @@ class ApiService {
priority = toPriority(message.priority), priority = toPriority(message.priority),
tags = joinTags(message.tags), tags = joinTags(message.tags),
click = message.click ?: "", click = message.click ?: "",
attachmentName = message.attachment?.name, attachment = attachment,
attachmentType = message.attachment?.type,
attachmentSize = message.attachment?.size,
attachmentExpires = message.attachment?.expires,
attachmentPreviewUrl = message.attachment?.preview_url,
attachmentUrl = message.attachment?.url,
attachmentContentUri = null,
notificationId = Random.nextInt(), notificationId = Random.nextInt(),
deleted = false deleted = false
) )
@ -149,6 +154,16 @@ class ApiService {
private fun fromString(subscriptionId: Long, s: String): Notification { private fun fromString(subscriptionId: Long, s: String): Notification {
val message = gson.fromJson(s, Message::class.java) val message = gson.fromJson(s, Message::class.java)
val attachment = if (message.attachment?.url != null) {
Attachment(
name = message.attachment.name,
type = message.attachment.type,
size = message.attachment.size,
expires = message.attachment.expires,
previewUrl = message.attachment.preview_url,
url = message.attachment.url,
)
} else null
return Notification( return Notification(
id = message.id, id = message.id,
subscriptionId = subscriptionId, subscriptionId = subscriptionId,
@ -158,14 +173,8 @@ class ApiService {
priority = toPriority(message.priority), priority = toPriority(message.priority),
tags = joinTags(message.tags), tags = joinTags(message.tags),
click = message.click ?: "", click = message.click ?: "",
attachmentName = message.attachment?.name, attachment = attachment,
attachmentType = message.attachment?.type, notificationId = 0, // zero!
attachmentSize = message.attachment?.size,
attachmentExpires = message.attachment?.expires,
attachmentPreviewUrl = message.attachment?.preview_url,
attachmentUrl = message.attachment?.url,
attachmentContentUri = null,
notificationId = 0,
deleted = false deleted = false
) )
} }

View file

@ -9,6 +9,7 @@ import android.util.Log
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
@ -34,15 +35,16 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
val repository = app.repository val repository = app.repository
val notification = repository.getNotification(notificationId) ?: return Result.failure() val notification = repository.getNotification(notificationId) ?: return Result.failure()
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure() val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
if (notification.attachmentPreviewUrl != null) { val attachment = notification.attachment ?: return Result.failure()
downloadPreview(repository, subscription, notification) if (attachment.previewUrl != null) {
downloadPreview(subscription, notification, attachment)
} }
downloadAttachment(repository, subscription, notification) downloadAttachment(repository, subscription, notification, attachment)
return Result.success() return Result.success()
} }
private fun downloadPreview(repository: Repository, subscription: Subscription, notification: Notification) { private fun downloadPreview(subscription: Subscription, notification: Notification, attachment: Attachment) {
val url = notification.attachmentPreviewUrl ?: return val url = attachment.previewUrl ?: return
Log.d(TAG, "Downloading preview from $url") Log.d(TAG, "Downloading preview from $url")
val request = Request.Builder() val request = Request.Builder()
@ -63,21 +65,20 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
} }
} }
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification) { private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) {
val url = notification.attachmentUrl ?: return Log.d(TAG, "Downloading attachment from ${attachment.url}")
Log.d(TAG, "Downloading attachment from $url")
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(attachment.url)
.addHeader("User-Agent", ApiService.USER_AGENT) .addHeader("User-Agent", ApiService.USER_AGENT)
.build() .build()
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
if (!response.isSuccessful || response.body == null) { if (!response.isSuccessful || response.body == null) {
throw Exception("Attachment download failed: ${response.code}") throw Exception("Attachment download failed: ${response.code}")
} }
val name = notification.attachmentName ?: "attachment.bin" val name = attachment.name ?: "attachment.bin"
val mimeType = notification.attachmentType ?: "application/octet-stream" val mimeType = attachment.type ?: "application/octet-stream"
val size = notification.attachmentSize ?: 0 val size = attachment.size ?: 0
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
val details = ContentValues().apply { val details = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.DISPLAY_NAME, name)
@ -107,7 +108,8 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
} }
} }
Log.d(TAG, "Attachment download: successful response, proceeding with download") Log.d(TAG, "Attachment download: successful response, proceeding with download")
val newNotification = notification.copy(attachmentContentUri = uri.toString()) val newAttachment = attachment.copy(contentUri = uri.toString())
val newNotification = notification.copy(attachment = newAttachment)
repository.updateNotification(newNotification) repository.updateNotification(newNotification)
notifier.update(subscription, newNotification) notifier.update(subscription, newNotification)
} }

View file

@ -1,6 +1,10 @@
package io.heckel.ntfy.msg package io.heckel.ntfy.msg
import android.content.Context import android.content.Context
import android.util.Log
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
@ -25,6 +29,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
val notify = shouldNotify(subscription, notification, muted) val notify = shouldNotify(subscription, notification, muted)
val broadcast = shouldBroadcast(subscription) val broadcast = shouldBroadcast(subscription)
val distribute = shouldDistribute(subscription) val distribute = shouldDistribute(subscription)
val download = shouldDownload(subscription, notification)
if (notify) { if (notify) {
notifier.display(subscription, notification) notifier.display(subscription, notification)
} }
@ -36,6 +41,16 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
distributor.sendMessage(appId, connectorToken, notification.message) distributor.sendMessage(appId, connectorToken, notification.message)
} }
} }
if (download) {
// Download attachment (+ preview if available) 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)
}
}
private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean {
return notification.attachment != null
} }
private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean { private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
@ -67,4 +82,19 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
} }
return subscription.mutedUntil == 1L || (subscription.mutedUntil > 1L && subscription.mutedUntil > System.currentTimeMillis()/1000) return subscription.mutedUntil == 1L || (subscription.mutedUntil > 1L && subscription.mutedUntil > System.currentTimeMillis()/1000)
} }
private fun scheduleAttachmentDownload(notification: Notification) {
Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)")
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
.setInputData(workDataOf(
"id" to notification.id,
))
.build()
workManager.enqueue(workRequest)
}
companion object {
private const val TAG = "NtfyNotifDispatch"
}
} }

View file

@ -32,16 +32,7 @@ class NotificationService(val context: Context) {
fun display(subscription: Subscription, notification: Notification) { fun display(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Displaying notification $notification") Log.d(TAG, "Displaying notification $notification")
// Display notification immediately
displayInternal(subscription, notification) displayInternal(subscription, notification)
// Download attachment (+ preview if available) 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.
if (notification.attachmentUrl != null) {
scheduleAttachmentDownload(subscription, notification)
}
} }
fun update(subscription: Subscription, notification: Notification, progress: Int = PROGRESS_NONE) { fun update(subscription: Subscription, notification: Notification, progress: Int = PROGRESS_NONE) {
@ -133,32 +124,21 @@ class NotificationService(val context: Context) {
} }
private fun maybeAddOpenAction(notificationBuilder: NotificationCompat.Builder, notification: Notification) { private fun maybeAddOpenAction(notificationBuilder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachmentContentUri != null) { if (notification.attachment?.contentUri != null) {
val contentUri = Uri.parse(notification.attachmentContentUri) val contentUri = Uri.parse(notification.attachment.contentUri)
val openIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, contentUri), 0) val openIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, contentUri), 0)
notificationBuilder.addAction(NotificationCompat.Action.Builder(0, "Open", openIntent).build()) notificationBuilder.addAction(NotificationCompat.Action.Builder(0, "Open", openIntent).build())
} }
} }
private fun maybeAddCopyUrlAction(builder: NotificationCompat.Builder, notification: Notification) { private fun maybeAddCopyUrlAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachmentUrl != null) { if (notification.attachment?.url != null) {
// XXXXXXXXx // XXXXXXXXx
val copyUrlIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse(notification.attachmentUrl)), 0) 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()) builder.addAction(NotificationCompat.Action.Builder(0, "Copy URL", copyUrlIntent).build())
} }
} }
private fun scheduleAttachmentDownload(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)")
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
.setInputData(workDataOf(
"id" to notification.id,
))
.build()
workManager.enqueue(workRequest)
}
private fun detailActivityIntent(subscription: Subscription): PendingIntent? { private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
val intent = Intent(context, DetailActivity::class.java) val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
@ -223,7 +203,7 @@ class NotificationService(val context: Context) {
const val PROGRESS_NONE = -1 const val PROGRESS_NONE = -1
const val PROGRESS_INDETERMINATE = -2 const val PROGRESS_INDETERMINATE = -2
private const val TAG = "NtfyNotificationService" private const val TAG = "NtfyNotifService"
private const val CHANNEL_ID_MIN = "ntfy-min" private const val CHANNEL_ID_MIN = "ntfy-min"
private const val CHANNEL_ID_LOW = "ntfy-low" private const val CHANNEL_ID_LOW = "ntfy-low"
private const val CHANNEL_ID_DEFAULT = "ntfy" private const val CHANNEL_ID_DEFAULT = "ntfy"

View file

@ -6,6 +6,7 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Attachment
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.* import io.heckel.ntfy.msg.*
import io.heckel.ntfy.service.SubscriberService import io.heckel.ntfy.service.SubscriberService
@ -81,6 +82,16 @@ class FirebaseService : FirebaseMessagingService() {
} }
// Add notification // Add notification
val attachment = if (attachmentUrl != null) {
Attachment(
name = attachmentName,
type = attachmentType,
size = attachmentSize,
expires = attachmentExpires,
previewUrl = attachmentPreviewUrl,
url = attachmentUrl,
)
} else null
val notification = Notification( val notification = Notification(
id = id, id = id,
subscriptionId = subscription.id, subscriptionId = subscription.id,
@ -90,13 +101,7 @@ class FirebaseService : FirebaseMessagingService() {
priority = toPriority(priority), priority = toPriority(priority),
tags = tags ?: "", tags = tags ?: "",
click = click ?: "", click = click ?: "",
attachmentName = attachmentName, attachment = attachment,
attachmentType = attachmentType,
attachmentSize = attachmentSize,
attachmentExpires = attachmentExpires,
attachmentPreviewUrl = attachmentPreviewUrl,
attachmentUrl = attachmentUrl,
attachmentContentUri = null,
notificationId = Random.nextInt(), notificationId = Random.nextInt(),
deleted = false deleted = false
) )