Implement UnifiedPush 2.0 spec (untested, #130)
This commit is contained in:
parent
81483ff3cd
commit
bd8d61997d
16 changed files with 376 additions and 35 deletions
|
@ -81,7 +81,7 @@ dependencies {
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
||||||
|
|
||||||
// Firebase, sigh ... (only Google Play)
|
// Firebase, sigh ... (only Google Play)
|
||||||
playImplementation 'com.google.firebase:firebase-messaging:23.0.1'
|
playImplementation 'com.google.firebase:firebase-messaging:23.0.0'
|
||||||
|
|
||||||
// RecyclerView
|
// RecyclerView
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.0-alpha01"
|
implementation "androidx.recyclerview:recyclerview:1.3.0-alpha01"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"identityHash": "eda2cb9740c4542f24462779eb6ff81d",
|
"identityHash": "5bab75c3b41c53c9855fe3a7ef8f0669",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "Subscription",
|
"tableName": "Subscription",
|
||||||
|
@ -82,7 +82,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, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, 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, `encoding` 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_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -114,6 +114,12 @@
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encoding",
|
||||||
|
"columnName": "encoding",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "notificationId",
|
"fieldPath": "notificationId",
|
||||||
"columnName": "notificationId",
|
"columnName": "notificationId",
|
||||||
|
@ -284,7 +290,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, 'eda2cb9740c4542f24462779eb6ff81d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bab75c3b41c53c9855fe3a7ef8f0669')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
296
app/schemas/io.heckel.ntfy.db.Database/9.json
Normal file
296
app/schemas/io.heckel.ntfy.db.Database/9.json
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "5bab75c3b41c53c9855fe3a7ef8f0669",
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_Subscription_upConnectorToken",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"upConnectorToken"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"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, `encoding` 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_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, 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": "encoding",
|
||||||
|
"columnName": "encoding",
|
||||||
|
"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": "deleted",
|
||||||
|
"columnName": "deleted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"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.url",
|
||||||
|
"columnName": "attachment_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.contentUri",
|
||||||
|
"columnName": "attachment_contentUri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachment.progress",
|
||||||
|
"columnName": "attachment_progress",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"subscriptionId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "User",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "baseUrl",
|
||||||
|
"columnName": "baseUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"baseUrl"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Log",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tag",
|
||||||
|
"columnName": "tag",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "level",
|
||||||
|
"columnName": "level",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "message",
|
||||||
|
"columnName": "message",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "exception",
|
||||||
|
"columnName": "exception",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"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, '5bab75c3b41c53c9855fe3a7ef8f0669')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,6 +108,7 @@
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="org.unifiedpush.android.distributor.REGISTER"/>
|
<action android:name="org.unifiedpush.android.distributor.REGISTER"/>
|
||||||
<action android:name="org.unifiedpush.android.distributor.UNREGISTER"/>
|
<action android:name="org.unifiedpush.android.distributor.UNREGISTER"/>
|
||||||
|
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ data class Notification(
|
||||||
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
||||||
@ColumnInfo(name = "title") val title: String,
|
@ColumnInfo(name = "title") val title: String,
|
||||||
@ColumnInfo(name = "message") val message: String,
|
@ColumnInfo(name = "message") val message: String,
|
||||||
|
@ColumnInfo(name = "encoding") val encoding: String, // "base64" or ""
|
||||||
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
|
@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 = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
|
||||||
@ColumnInfo(name = "tags") val tags: String,
|
@ColumnInfo(name = "tags") val tags: String,
|
||||||
|
@ -100,7 +101,7 @@ data class LogEntry(
|
||||||
this(0, timestamp, tag, level, message, exception)
|
this(0, timestamp, tag, level, message, exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 8)
|
@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, LogEntry::class], version = 9)
|
||||||
abstract class Database : RoomDatabase() {
|
abstract class Database : RoomDatabase() {
|
||||||
abstract fun subscriptionDao(): SubscriptionDao
|
abstract fun subscriptionDao(): SubscriptionDao
|
||||||
abstract fun notificationDao(): NotificationDao
|
abstract fun notificationDao(): NotificationDao
|
||||||
|
@ -122,6 +123,7 @@ abstract class Database : RoomDatabase() {
|
||||||
.addMigrations(MIGRATION_5_6)
|
.addMigrations(MIGRATION_5_6)
|
||||||
.addMigrations(MIGRATION_6_7)
|
.addMigrations(MIGRATION_6_7)
|
||||||
.addMigrations(MIGRATION_7_8)
|
.addMigrations(MIGRATION_7_8)
|
||||||
|
.addMigrations(MIGRATION_8_9)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
this.instance = instance
|
this.instance = instance
|
||||||
|
@ -191,6 +193,12 @@ abstract class Database : RoomDatabase() {
|
||||||
db.execSQL("CREATE TABLE User (baseUrl TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, PRIMARY KEY(baseUrl))")
|
db.execSQL("CREATE TABLE User (baseUrl TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, PRIMARY KEY(baseUrl))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_8_9 = object : Migration(8, 9) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("ALTER TABLE Notification ADD COLUMN encoding TEXT NOT NULL DEFAULT('')")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,12 @@ package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Base64
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.*
|
||||||
import io.heckel.ntfy.util.joinTagsMap
|
|
||||||
import io.heckel.ntfy.util.splitTags
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -26,7 +25,9 @@ class BroadcastService(private val ctx: Context) {
|
||||||
intent.putExtra("topic", subscription.topic)
|
intent.putExtra("topic", subscription.topic)
|
||||||
intent.putExtra("time", notification.timestamp.toInt())
|
intent.putExtra("time", notification.timestamp.toInt())
|
||||||
intent.putExtra("title", notification.title)
|
intent.putExtra("title", notification.title)
|
||||||
intent.putExtra("message", notification.message)
|
intent.putExtra("message", decodeMessage(notification))
|
||||||
|
intent.putExtra("message_bytes", decodeBytesMessage(notification))
|
||||||
|
intent.putExtra("message_encoding", notification.encoding)
|
||||||
intent.putExtra("tags", notification.tags)
|
intent.putExtra("tags", notification.tags)
|
||||||
intent.putExtra("tags_map", joinTagsMap(splitTags(notification.tags)))
|
intent.putExtra("tags_map", joinTagsMap(splitTags(notification.tags)))
|
||||||
intent.putExtra("priority", notification.priority)
|
intent.putExtra("priority", notification.priority)
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.up.Distributor
|
import io.heckel.ntfy.up.Distributor
|
||||||
|
import io.heckel.ntfy.util.decodeBytesMessage
|
||||||
import io.heckel.ntfy.util.safeLet
|
import io.heckel.ntfy.util.safeLet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,7 +39,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
if (distribute) {
|
if (distribute) {
|
||||||
safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
|
safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
|
||||||
distributor.sendMessage(appId, connectorToken, notification.message)
|
distributor.sendMessage(appId, connectorToken, decodeBytesMessage(notification))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (download) {
|
if (download) {
|
||||||
|
|
|
@ -20,11 +20,6 @@ class NotificationParser {
|
||||||
if (message.event != ApiService.EVENT_MESSAGE) {
|
if (message.event != ApiService.EVENT_MESSAGE) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val decodedMessage = if (message.encoding == MESSAGE_ENCODING_BASE64) {
|
|
||||||
String(Base64.decode(message.message, Base64.DEFAULT))
|
|
||||||
} else {
|
|
||||||
message.message
|
|
||||||
}
|
|
||||||
val attachment = if (message.attachment?.url != null) {
|
val attachment = if (message.attachment?.url != null) {
|
||||||
Attachment(
|
Attachment(
|
||||||
name = message.attachment.name,
|
name = message.attachment.name,
|
||||||
|
@ -39,7 +34,8 @@ class NotificationParser {
|
||||||
subscriptionId = subscriptionId,
|
subscriptionId = subscriptionId,
|
||||||
timestamp = message.time,
|
timestamp = message.time,
|
||||||
title = message.title ?: "",
|
title = message.title ?: "",
|
||||||
message = decodedMessage,
|
message = message.message,
|
||||||
|
encoding = message.encoding ?: "",
|
||||||
priority = toPriority(message.priority),
|
priority = toPriority(message.priority),
|
||||||
tags = joinTags(message.tags),
|
tags = joinTags(message.tags),
|
||||||
click = message.click ?: "",
|
click = message.click ?: "",
|
||||||
|
|
|
@ -39,7 +39,7 @@ class NotificationService(val context: Context) {
|
||||||
|
|
||||||
fun cancel(notification: Notification) {
|
fun cancel(notification: Notification) {
|
||||||
if (notification.notificationId != 0) {
|
if (notification.notificationId != 0) {
|
||||||
Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}")
|
Log.d(TAG, "Cancelling notification ${notification.id}: ${decodeMessage(notification)}")
|
||||||
notificationManager.cancel(notification.notificationId)
|
notificationManager.cancel(notification.notificationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,8 +87,8 @@ class JsonConnection(
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
Log.d(TAG, "[$url] Cancelling connection")
|
Log.d(TAG, "[$url] Cancelling connection")
|
||||||
if (this::job.isInitialized) job?.cancel()
|
if (this::job.isInitialized) job.cancel()
|
||||||
if (this::call.isInitialized) call?.cancel()
|
if (this::call.isInitialized) call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
|
private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
|
import android.util.Base64
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -29,6 +30,7 @@ import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
|
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
||||||
import io.heckel.ntfy.msg.NotificationService
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
|
@ -514,9 +516,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
|
|
||||||
private fun copyToClipboard(notification: Notification) {
|
private fun copyToClipboard(notification: Notification) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val message = notification.message + "\n\n" + Date(notification.timestamp * 1000).toString()
|
val message = decodeMessage(notification)
|
||||||
|
val text = message + "\n\n" + Date(notification.timestamp * 1000).toString()
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("notification message", message)
|
val clip = ClipData.newPlainText("notification message", text)
|
||||||
clipboard.setPrimaryClip(clip)
|
clipboard.setPrimaryClip(clip)
|
||||||
Toast
|
Toast
|
||||||
.makeText(this, getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
.makeText(this, getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG)
|
||||||
|
@ -574,7 +577,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
val content = adapter.selected.joinToString("\n\n") { notificationId ->
|
val content = adapter.selected.joinToString("\n\n") { notificationId ->
|
||||||
val notification = repository.getNotification(notificationId)
|
val notification = repository.getNotification(notificationId)
|
||||||
notification?.let {
|
notification?.let {
|
||||||
it.message + "\n" + Date(it.timestamp * 1000).toString()
|
decodeMessage(it) + "\n" + Date(it.timestamp * 1000).toString()
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
|
|
@ -13,7 +13,10 @@ const val ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"
|
||||||
const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"
|
const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"
|
||||||
const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"
|
const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"
|
||||||
|
|
||||||
|
const val FEATURE_BYTES_MESSAGE = "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"
|
||||||
|
|
||||||
const val EXTRA_APPLICATION = "application"
|
const val EXTRA_APPLICATION = "application"
|
||||||
const val EXTRA_TOKEN = "token"
|
const val EXTRA_TOKEN = "token"
|
||||||
const val EXTRA_ENDPOINT = "endpoint"
|
const val EXTRA_ENDPOINT = "endpoint"
|
||||||
const val EXTRA_MESSAGE = "message"
|
const val EXTRA_MESSAGE = "message"
|
||||||
|
const val EXTRA_BYTES_MESSAGE = "bytesMessage"
|
||||||
|
|
|
@ -9,13 +9,14 @@ import io.heckel.ntfy.util.Log
|
||||||
* See https://unifiedpush.org/spec/android/ for details.
|
* See https://unifiedpush.org/spec/android/ for details.
|
||||||
*/
|
*/
|
||||||
class Distributor(val context: Context) {
|
class Distributor(val context: Context) {
|
||||||
fun sendMessage(app: String, connectorToken: String, message: String) {
|
fun sendMessage(app: String, connectorToken: String, message: ByteArray) {
|
||||||
Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): $message")
|
Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): ${String(message)} (${message.size} bytes)}")
|
||||||
val broadcastIntent = Intent()
|
val broadcastIntent = Intent()
|
||||||
broadcastIntent.`package` = app
|
broadcastIntent.`package` = app
|
||||||
broadcastIntent.action = ACTION_MESSAGE
|
broadcastIntent.action = ACTION_MESSAGE
|
||||||
broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
|
broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
|
||||||
broadcastIntent.putExtra(EXTRA_MESSAGE, message)
|
broadcastIntent.putExtra(EXTRA_MESSAGE, String(message)) // UTF-8
|
||||||
|
broadcastIntent.putExtra(EXTRA_BYTES_MESSAGE, message)
|
||||||
context.sendBroadcast(broadcastIntent)
|
context.sendBroadcast(broadcastIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import android.os.PowerManager
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.util.Base64
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
|
@ -22,6 +23,7 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
|
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
@ -110,21 +112,45 @@ fun unmatchedTags(tags: List<String>): List<String> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend tags/emojis to message, but only if there is a non-empty title.
|
* Prepend tags/emojis to message, but only if there is a non-empty title.
|
||||||
* Otherwise the tags will be prepended to the title.
|
* Otherwise, the tags will be prepended to the title.
|
||||||
*/
|
*/
|
||||||
fun formatMessage(notification: Notification): String {
|
fun formatMessage(notification: Notification): String {
|
||||||
return if (notification.title != "") {
|
return if (notification.title != "") {
|
||||||
notification.message
|
decodeMessage(notification)
|
||||||
} else {
|
} else {
|
||||||
val emojis = toEmojis(splitTags(notification.tags))
|
val emojis = toEmojis(splitTags(notification.tags))
|
||||||
if (emojis.isEmpty()) {
|
if (emojis.isEmpty()) {
|
||||||
notification.message
|
decodeMessage(notification)
|
||||||
} else {
|
} else {
|
||||||
emojis.joinToString("") + " " + notification.message
|
emojis.joinToString("") + " " + decodeMessage(notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeMessage(notification: Notification): String {
|
||||||
|
return try {
|
||||||
|
if (notification.encoding == MESSAGE_ENCODING_BASE64) {
|
||||||
|
String(Base64.decode(notification.message, Base64.DEFAULT))
|
||||||
|
} else {
|
||||||
|
notification.message
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
notification.message + "(invalid base64)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeBytesMessage(notification: Notification): ByteArray {
|
||||||
|
return try {
|
||||||
|
if (notification.encoding == MESSAGE_ENCODING_BASE64) {
|
||||||
|
Base64.decode(notification.message, Base64.DEFAULT)
|
||||||
|
} else {
|
||||||
|
notification.message.toByteArray()
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
notification.message.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See above; prepend emojis to title if the title is non-empty.
|
* See above; prepend emojis to title if the title is non-empty.
|
||||||
* Otherwise, they are prepended to the message.
|
* Otherwise, they are prepended to the message.
|
||||||
|
|
|
@ -112,11 +112,6 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add notification
|
// Add notification
|
||||||
val decodedMessage = if (encoding == MESSAGE_ENCODING_BASE64) {
|
|
||||||
String(Base64.decode(message, Base64.DEFAULT))
|
|
||||||
} else {
|
|
||||||
message
|
|
||||||
}
|
|
||||||
val attachment = if (attachmentUrl != null) {
|
val attachment = if (attachmentUrl != null) {
|
||||||
Attachment(
|
Attachment(
|
||||||
name = attachmentName,
|
name = attachmentName,
|
||||||
|
@ -131,7 +126,8 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
subscriptionId = subscription.id,
|
subscriptionId = subscription.id,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
title = title ?: "",
|
title = title ?: "",
|
||||||
message = decodedMessage,
|
message = message,
|
||||||
|
encoding = encoding ?: "",
|
||||||
priority = toPriority(priority),
|
priority = toPriority(priority),
|
||||||
tags = tags ?: "",
|
tags = tags ?: "",
|
||||||
click = click ?: "",
|
click = click ?: "",
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelog/24.txt
Normal file
2
fastlane/metadata/android/en-US/changelog/24.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Features:
|
||||||
|
* Support for UnifiedPush 2.0 specification (bytes messages, #130)
|
Loading…
Reference in a new issue