diff --git a/app/schemas/io.heckel.ntfy.data.Database/5.json b/app/schemas/io.heckel.ntfy.data.Database/5.json index 2131068..228cf1f 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": "fd7d1e0ac6ac7d68eb79ffe928dae67a", + "identityHash": "f662a6c15e0b9e510350918228bfa0ea", "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, `attachmentSize` INTEGER, `attachmentExpires` INTEGER, `attachmentPreviewUrl` TEXT, `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, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))", "fields": [ { "fieldPath": "id", @@ -132,40 +132,10 @@ "notNull": true }, { - "fieldPath": "attachmentName", - "columnName": "attachmentName", + "fieldPath": "click", + "columnName": "click", "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 + "notNull": true }, { "fieldPath": "deleted", @@ -188,7 +158,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, 'fd7d1e0ac6ac7d68eb79ffe928dae67a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f662a6c15e0b9e510350918228bfa0ea')" ] } } \ No newline at end of file diff --git a/app/schemas/io.heckel.ntfy.data.Database/6.json b/app/schemas/io.heckel.ntfy.data.Database/6.json new file mode 100644 index 0000000..1a7ffd2 --- /dev/null +++ b/app/schemas/io.heckel.ntfy.data.Database/6.json @@ -0,0 +1,200 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "0c64bd96a759eb0d899cd251756d6c00", + "entities": [ + { + "tableName": "Subscription", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `upAppId` TEXT, `upConnectorToken` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instant", + "columnName": "instant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mutedUntil", + "columnName": "mutedUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upAppId", + "columnName": "upAppId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "upConnectorToken", + "columnName": "upConnectorToken", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_Subscription_baseUrl_topic", + "unique": true, + "columnNames": [ + "baseUrl", + "topic" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)" + }, + { + "name": "index_Subscription_upConnectorToken", + "unique": true, + "columnNames": [ + "upConnectorToken" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)" + } + ], + "foreignKeys": [] + }, + { + "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, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`, `subscriptionId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscriptionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "click", + "columnName": "click", + "affinity": "TEXT", + "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": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "subscriptionId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, '0c64bd96a759eb0d899cd251756d6c00')" + ] + } +} \ 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 c5e8b57..e26abbb 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -13,8 +13,8 @@ data class Subscription( @ColumnInfo(name = "topic") val topic: String, @ColumnInfo(name = "instant") val instant: Boolean, @ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule - @ColumnInfo(name = "upAppId") val upAppId: String?, - @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, + @ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name + @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token @Ignore val totalCount: Int = 0, // Total notifications @Ignore val newCount: Int = 0, // New notifications @Ignore val lastActive: Long = 0, // Unix timestamp @@ -51,6 +51,7 @@ 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 = "click") val click: String, // URL/intent to open on notification click @ColumnInfo(name = "attachmentName") val attachmentName: String?, // Filename @ColumnInfo(name = "attachmentType") val attachmentType: String?, // MIME type @ColumnInfo(name = "attachmentSize") val attachmentSize: Long?, // Size in bytes @@ -60,7 +61,7 @@ data class Notification( @ColumnInfo(name = "deleted") val deleted: Boolean, ) -@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 5) +@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6) abstract class Database : RoomDatabase() { abstract fun subscriptionDao(): SubscriptionDao abstract fun notificationDao(): NotificationDao @@ -77,6 +78,7 @@ abstract class Database : RoomDatabase() { .addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_3_4) .addMigrations(MIGRATION_4_5) + .addMigrations(MIGRATION_5_6) .fallbackToDestructiveMigration() .build() this.instance = instance @@ -121,6 +123,12 @@ abstract class Database : RoomDatabase() { db.execSQL("CREATE UNIQUE INDEX index_Subscription_upConnectorToken ON Subscription (upConnectorToken)") } } + + private val MIGRATION_5_6 = object : Migration(5, 6) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Notification ADD COLUMN click TEXT NOT NULL DEFAULT('')") + } + } } } 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 5bacbed..7c0c4e0 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,7 @@ class ApiService { message = message.message, priority = toPriority(message.priority), tags = joinTags(message.tags), + click = message.click ?: "", attachmentName = message.attachment?.name, attachmentType = message.attachment?.type, attachmentSize = message.attachment?.size, @@ -155,6 +156,7 @@ class ApiService { message = message.message, priority = toPriority(message.priority), tags = joinTags(message.tags), + click = message.click ?: "", attachmentName = message.attachment?.name, attachmentType = message.attachment?.type, attachmentSize = message.attachment?.size, @@ -176,6 +178,7 @@ class ApiService { val topic: String, val priority: Int?, val tags: List?, + val click: String?, val title: String?, val message: String, val attachment: Attachment?, 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 83e1a8e..e8f525c 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -27,7 +27,6 @@ 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 @@ -59,18 +58,6 @@ class NotificationService(val context: Context) { } 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) - intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) - intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) - intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) - intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) - val pendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack - getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack - } - val title = formatTitle(subscription, notification) val message = formatMessage(notification) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) @@ -81,14 +68,15 @@ class NotificationService(val context: Context) { .setContentTitle(title) .setContentText(message) .setSound(defaultSoundUri) - .setContentIntent(pendingIntent) // Click target for notification .setAutoCancel(true) // Cancel when notification is clicked + notificationBuilder = setContentIntent(notificationBuilder, subscription, notification) + 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()) + .addAction(NotificationCompat.Action.Builder(0, "Download", viewIntent).build()) } notificationBuilder = if (bitmap != null) { notificationBuilder @@ -124,6 +112,18 @@ class NotificationService(val context: Context) { } } + private fun setContentIntent(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification): NotificationCompat.Builder? { + if (notification.click == "") { + return builder.setContentIntent(detailActivityIntent(subscription)) + } + return try { + val uri = Uri.parse(notification.click) + val viewIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, uri), 0) + builder.setContentIntent(viewIntent) + } catch (e: Exception) { + builder.setContentIntent(detailActivityIntent(subscription)) + } + } private fun downloadPreviewAndUpdateXXX(subscription: Subscription, notification: Notification) { val url = notification.attachmentUrl ?: return @@ -151,6 +151,19 @@ class NotificationService(val context: Context) { } } + private fun detailActivityIntent(subscription: Subscription): PendingIntent? { + val intent = Intent(context, DetailActivity::class.java) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) + return TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack + getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack + } + } + private fun maybeCreateNotificationChannel(notificationManager: NotificationManager, priority: Int) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Note: To change a notification channel, you must delete the old one and create a new one! 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 3ebdd9a..9645329 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -56,23 +56,31 @@ class FirebaseService : FirebaseMessagingService() { val message = data["message"] val priority = data["priority"]?.toIntOrNull() val tags = data["tags"] + val click = data["click"] val attachmentName = data["attachment_name"] 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"] + val truncated = (data["truncated"] ?: "") == "1" if (id == null || topic == null || message == null || timestamp == null) { - Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}") + Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}") return } - Log.d(TAG, "Received notification: from=${remoteMessage.from}, data=${data}") + Log.d(TAG, "Received message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}") CoroutineScope(job).launch { val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL! - // Add notification + // Check if notification was truncated and discard if it will (or likely already did) arrive via instant delivery val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch + if (truncated && subscription.instant) { + Log.d(TAG, "Discarding truncated message that did/will arrive via instant delivery: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}") + return@launch + } + + // Add notification val notification = Notification( id = id, subscriptionId = subscription.id, @@ -81,6 +89,7 @@ class FirebaseService : FirebaseMessagingService() { message = message, priority = toPriority(priority), tags = tags ?: "", + click = click ?: "", attachmentName = attachmentName, attachmentType = attachmentType, attachmentSize = attachmentSize, @@ -91,7 +100,7 @@ class FirebaseService : FirebaseMessagingService() { deleted = false ) if (repository.addNotification(notification)) { - Log.d(TAG, "Dispatching notification for message: from=${remoteMessage.from}, data=${data}") + Log.d(TAG, "Dispatching notification for message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}") dispatcher.dispatch(subscription, notification) } }