diff --git a/app/schemas/io.heckel.ntfy.data.Database/5.json b/app/schemas/io.heckel.ntfy.data.Database/5.json index 0620854..dd398a4 100644 --- a/app/schemas/io.heckel.ntfy.data.Database/5.json +++ b/app/schemas/io.heckel.ntfy.data.Database/5.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 5, - "identityHash": "d72d045ad4ad20db887b4c6aed3da27b", + "identityHash": "306578182c2ad0f9803956beda094d28", "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`))", + "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", @@ -42,13 +42,13 @@ "fieldPath": "upAppId", "columnName": "upAppId", "affinity": "TEXT", - "notNull": true + "notNull": false }, { "fieldPath": "upConnectorToken", "columnName": "upConnectorToken", "affinity": "TEXT", - "notNull": true + "notNull": false } ], "primaryKey": { @@ -66,6 +66,14 @@ "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": [] @@ -144,7 +152,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, 'd72d045ad4ad20db887b4c6aed3da27b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '306578182c2ad0f9803956beda094d28')" ] } } \ 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 240342b..ad87a96 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -6,15 +6,15 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import kotlinx.coroutines.flow.Flow -@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true)]) +@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true), Index(value = ["upConnectorToken"], unique = true)]) data class Subscription( @PrimaryKey val id: Long, // Internal ID, only used in Repository and activities @ColumnInfo(name = "baseUrl") val baseUrl: String, @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?, + @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, @Ignore val totalCount: Int = 0, // Total notifications @Ignore val newCount: Int = 0, // New notifications @Ignore val lastActive: Long = 0, // Unix timestamp @@ -110,8 +110,8 @@ abstract class Database : RoomDatabase() { private val MIGRATION_4_5 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE Subscription ADD COLUMN upAppId TEXT NOT NULL DEFAULT('')") - db.execSQL("ALTER TABLE Subscription ADD COLUMN upConnectorToken TEXT NOT NULL DEFAULT('')") + db.execSQL("ALTER TABLE Subscription ADD COLUMN upAppId TEXT") + db.execSQL("ALTER TABLE Subscription ADD COLUMN upConnectorToken TEXT") } } } @@ -166,11 +166,24 @@ interface SubscriptionDao { IFNULL(MAX(n.timestamp),0) AS lastActive FROM Subscription AS s LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 - WHERE s.id = :subscriptionId + WHERE s.id = :subscriptionId GROUP BY s.id """) fun get(subscriptionId: Long): SubscriptionWithMetadata? + @Query(""" + SELECT + s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.upAppId, s.upConnectorToken, + COUNT(n.id) totalCount, + COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, + IFNULL(MAX(n.timestamp),0) AS lastActive + FROM Subscription AS s + LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 + WHERE s.upConnectorToken = :connectorToken + GROUP BY s.id + """) + fun getByConnectorToken(connectorToken: String): SubscriptionWithMetadata? + @Insert fun add(subscription: Subscription) diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index fa6797a..1d1a2cc 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -54,6 +54,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri return toSubscription(subscriptionDao.get(baseUrl, topic)) } + @Suppress("RedundantSuspendModifier") + @WorkerThread + suspend fun getSubscriptionByConnectorToken(connectorToken: String): Subscription? { + return toSubscription(subscriptionDao.getByConnectorToken(connectorToken)) + } + @Suppress("RedundantSuspendModifier") @WorkerThread suspend fun addSubscription(subscription: Subscription) { diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt index d4bade2..aa251cd 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -5,6 +5,7 @@ import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.up.Distributor +import io.heckel.ntfy.util.safeLet class NotificationDispatcher(val context: Context, val repository: Repository) { private val notifier = NotificationService(context) @@ -18,8 +19,8 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { fun dispatch(subscription: Subscription, notification: Notification) { val muted = checkMuted(subscription) val notify = checkNotify(subscription, notification, muted) - val broadcast = subscription.upAppId == "" - val distribute = subscription.upAppId != "" + val broadcast = subscription.upAppId == null + val distribute = subscription.upAppId != null if (notify) { notifier.send(subscription, notification) } @@ -27,7 +28,9 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { broadcaster.send(subscription, notification, muted) } if (distribute) { - distributor.sendMessage(subscription.upAppId, subscription.upConnectorToken, notification.message) + safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken -> + distributor.sendMessage(appId, connectorToken, notification.message) + } } } diff --git a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt index 71c36bb..125c766 100644 --- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt +++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt @@ -7,6 +7,8 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.ui.SubscriberManager +import io.heckel.ntfy.util.randomString +import io.heckel.ntfy.util.topicUrlUp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -24,44 +26,62 @@ class BroadcastReceiver : android.content.BroadcastReceiver() { 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 topic = "up" + randomString(TOPIC_LENGTH) + val endpoint = topicUrlUp(baseUrl, topic) val app = context!!.applicationContext as Application val repository = app.repository val distributor = Distributor(app) - 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) { + val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) + if (existingSubscription != null) { + distributor.sendRegistrationRefused(appId, connectorToken) + return@launch + } + val subscription = Subscription( + id = Random.nextLong(), + baseUrl = baseUrl, + topic = topic, + instant = true, // No Firebase, always instant! + mutedUntil = 0, + upAppId = appId, + upConnectorToken = connectorToken, + totalCount = 0, + newCount = 0, + lastActive = Date().time/1000 + ) repository.addSubscription(subscription) val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() - val subscriberManager = SubscriberManager(context!!) + val subscriberManager = SubscriberManager(app) subscriberManager.refreshService(subscriptionIdsWithInstantStatus) + distributor.sendEndpoint(appId, connectorToken, endpoint) } - distributor.sendEndpoint(appId, connectorToken) - // XXXXXXXXX } ACTION_UNREGISTER -> { val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" Log.d(TAG, "Unregister: connectorToken=$connectorToken") - // XXXXXXX - val distributor = Distributor(context!!) - distributor.sendUnregistered("org.unifiedpush.example", connectorToken) + val app = context!!.applicationContext as Application + val repository = app.repository + val distributor = Distributor(app) + GlobalScope.launch(Dispatchers.IO) { + val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken) + if (existingSubscription == null) { + return@launch + } + repository.removeSubscription(existingSubscription.id) + val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() + val subscriberManager = SubscriberManager(app) + subscriberManager.refreshService(subscriptionIdsWithInstantStatus) + existingSubscription.upAppId?.let { appId -> + distributor.sendUnregistered(appId, connectorToken) + } + } } } } companion object { private const val TAG = "NtfyUpBroadcastRecv" + private const val TOPIC_LENGTH = 16 } } diff --git a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt index 975ab58..9bdaf7e 100644 --- a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt +++ b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt @@ -2,35 +2,39 @@ package io.heckel.ntfy.up import android.content.Context import android.content.Intent -import io.heckel.ntfy.R -import io.heckel.ntfy.data.Repository -import io.heckel.ntfy.util.topicUrlUp class Distributor(val context: Context) { - fun sendMessage(app: String, token: String, message: String) { + fun sendMessage(app: String, connectorToken: String, message: String) { val broadcastIntent = Intent() broadcastIntent.`package` = app broadcastIntent.action = ACTION_MESSAGE - broadcastIntent.putExtra(EXTRA_TOKEN, token) + broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken) broadcastIntent.putExtra(EXTRA_MESSAGE, message) context.sendBroadcast(broadcastIntent) } - fun sendEndpoint(app: String, token: String) { - val appBaseUrl = context.getString(R.string.app_base_url) // FIXME + fun sendEndpoint(app: String, connectorToken: String, endpoint: String) { val broadcastIntent = Intent() broadcastIntent.`package` = app broadcastIntent.action = ACTION_NEW_ENDPOINT - broadcastIntent.putExtra(EXTRA_TOKEN, token) - broadcastIntent.putExtra(EXTRA_ENDPOINT, topicUrlUp(appBaseUrl, token)) + broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken) + broadcastIntent.putExtra(EXTRA_ENDPOINT, endpoint) context.sendBroadcast(broadcastIntent) } - fun sendUnregistered(app: String, token: String) { + fun sendUnregistered(app: String, connectorToken: String) { val broadcastIntent = Intent() broadcastIntent.`package` = app broadcastIntent.action = ACTION_UNREGISTERED - broadcastIntent.putExtra(EXTRA_TOKEN, token) + broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken) + context.sendBroadcast(broadcastIntent) + } + + fun sendRegistrationRefused(app: String, connectorToken: String) { + val broadcastIntent = Intent() + broadcastIntent.`package` = app + broadcastIntent.action = ACTION_REGISTRATION_REFUSED + broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken) 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 1d8dcff..1b88747 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -5,6 +5,7 @@ import android.animation.ValueAnimator import android.view.Window import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Subscription +import java.security.SecureRandom import java.text.DateFormat import java.util.* @@ -102,3 +103,14 @@ fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { } statusBarColorAnimation.start() } + +fun randomString(len: Int): String { + val random = SecureRandom() + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray() + return (1..len).map { chars[random.nextInt(chars.size)] }.joinToString("") +} + +// Allows letting multiple variables at once, see https://stackoverflow.com/a/35522422/1440785 +inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { + return if (p1 != null && p2 != null) block(p1, p2) else null +}