Implement UnifiedPush 2.0 spec (untested, #130)

This commit is contained in:
Philipp Heckel 2022-03-13 15:58:19 -04:00
parent 81483ff3cd
commit bd8d61997d
16 changed files with 376 additions and 35 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
Features:
* Support for UnifiedPush 2.0 specification (bytes messages, #130)