diff --git a/app/schemas/io.heckel.ntfy.data.Database/5.json b/app/schemas/io.heckel.ntfy.data.Database/5.json
new file mode 100644
index 0000000..0620854
--- /dev/null
+++ b/app/schemas/io.heckel.ntfy.data.Database/5.json
@@ -0,0 +1,150 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "d72d045ad4ad20db887b4c6aed3da27b",
+ "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 NOT NULL, `upConnectorToken` TEXT NOT NULL, 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": true
+ },
+ {
+ "fieldPath": "upConnectorToken",
+ "columnName": "upConnectorToken",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "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`)"
+ }
+ ],
+ "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, `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": "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, 'd72d045ad4ad20db887b4c6aed3da27b')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 931bab1..1beaca1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -64,6 +64,14 @@
+
+
+
+
+
+
+
+
{
+ val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: ""
+ val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: ""
+ Log.d(TAG, "Register: app=$appId, connectorToken=$connectorToken")
+ if (appId.isBlank()) {
+ Log.w(TAG, "Trying to register an app without packageName")
+ return
+ }
+
+ val baseUrl = context!!.getString(R.string.app_base_url) // FIXME
+ val topic = connectorToken // FIXME
+ val app = context!!.applicationContext as Application
+ val repository = app.repository
+ val subscription = Subscription(
+ id = Random.nextLong(),
+ baseUrl = baseUrl,
+ topic = topic,
+ instant = true,
+ mutedUntil = 0,
+ upAppId = appId,
+ upConnectorToken = connectorToken,
+ totalCount = 0,
+ newCount = 0,
+ lastActive = Date().time/1000
+ )
+ GlobalScope.launch(Dispatchers.IO) {
+ repository.addSubscription(subscription)
+ }
+
+ sendEndpoint(context!!, appId, connectorToken)
+ // XXXXXXXXX
+ }
+ ACTION_UNREGISTER -> {
+ val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: ""
+ Log.d(TAG, "Unregister: connectorToken=$connectorToken")
+ // XXXXXXX
+ sendUnregistered(context!!, "org.unifiedpush.example", connectorToken)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "NtfyUpBroadcastRecv"
+ }
+}
diff --git a/app/src/main/java/io/heckel/ntfy/up/Constants.kt b/app/src/main/java/io/heckel/ntfy/up/Constants.kt
new file mode 100644
index 0000000..6ee8b41
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/up/Constants.kt
@@ -0,0 +1,22 @@
+package io.heckel.ntfy.up
+
+/**
+ * Constants as defined on the specs
+ * https://github.com/UnifiedPush/UP-spec/blob/main/specifications.md
+ */
+
+const val ACTION_NEW_ENDPOINT = "org.unifiedpush.android.connector.NEW_ENDPOINT"
+const val ACTION_REGISTRATION_FAILED = "org.unifiedpush.android.connector.REGISTRATION_FAILED"
+const val ACTION_REGISTRATION_REFUSED = "org.unifiedpush.android.connector.REGISTRATION_REFUSED"
+const val ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED"
+const val ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"
+
+const val ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"
+const val ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"
+const val ACTION_MESSAGE_ACK = "org.unifiedpush.android.distributor.MESSAGE_ACK"
+
+const val EXTRA_APPLICATION = "application"
+const val EXTRA_TOKEN = "token"
+const val EXTRA_ENDPOINT = "endpoint"
+const val EXTRA_MESSAGE = "message"
+const val EXTRA_MESSAGE_ID = "id"
diff --git a/app/src/main/java/io/heckel/ntfy/up/DistributorUtils.kt b/app/src/main/java/io/heckel/ntfy/up/DistributorUtils.kt
new file mode 100644
index 0000000..a7ba475
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/up/DistributorUtils.kt
@@ -0,0 +1,34 @@
+package io.heckel.ntfy.up
+
+import android.content.Context
+import android.content.Intent
+import io.heckel.ntfy.R
+import io.heckel.ntfy.util.topicUrlUp
+
+fun sendMessage(context: Context, app: String, token: String, message: String) {
+ val broadcastIntent = Intent()
+ broadcastIntent.`package` = app
+ broadcastIntent.action = ACTION_MESSAGE
+ broadcastIntent.putExtra(EXTRA_TOKEN, token)
+ broadcastIntent.putExtra(EXTRA_MESSAGE, message)
+ context.sendBroadcast(broadcastIntent)
+}
+
+fun sendEndpoint(context: Context, app: String, token: String) {
+ val appBaseUrl = context.getString(R.string.app_base_url)
+ val broadcastIntent = Intent()
+ broadcastIntent.`package` = app
+ broadcastIntent.action = ACTION_NEW_ENDPOINT
+ broadcastIntent.putExtra(EXTRA_TOKEN, token)
+ broadcastIntent.putExtra(EXTRA_ENDPOINT, topicUrlUp(appBaseUrl, token))
+ context.sendBroadcast(broadcastIntent)
+}
+
+fun sendUnregistered(context: Context, app: String, token: String) {
+ val broadcastIntent = Intent()
+ broadcastIntent.`package` = app
+ broadcastIntent.action = ACTION_UNREGISTERED
+ broadcastIntent.putExtra(EXTRA_TOKEN, token)
+ context.sendBroadcast(broadcastIntent)
+}
+
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
index b5f4a08..1d8dcff 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -9,6 +9,7 @@ import java.text.DateFormat
import java.util.*
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
+fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
fun topicUrlJsonPoll(baseUrl: String, topic: String) = "${topicUrl(baseUrl, topic)}/json?poll=1"
fun topicShortUrl(baseUrl: String, topic: String) =