Full end to end use case works; still ugly though

This commit is contained in:
Philipp Heckel 2021-12-30 01:05:32 +01:00
parent 7dbbf12c99
commit 73f610afa8
7 changed files with 111 additions and 45 deletions

View file

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 5, "version": 5,
"identityHash": "d72d045ad4ad20db887b4c6aed3da27b", "identityHash": "306578182c2ad0f9803956beda094d28",
"entities": [ "entities": [
{ {
"tableName": "Subscription", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -42,13 +42,13 @@
"fieldPath": "upAppId", "fieldPath": "upAppId",
"columnName": "upAppId", "columnName": "upAppId",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": false
}, },
{ {
"fieldPath": "upConnectorToken", "fieldPath": "upConnectorToken",
"columnName": "upConnectorToken", "columnName": "upConnectorToken",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -66,6 +66,14 @@
"topic" "topic"
], ],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`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": [] "foreignKeys": []
@ -144,7 +152,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, 'd72d045ad4ad20db887b4c6aed3da27b')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '306578182c2ad0f9803956beda094d28')"
] ]
} }
} }

View file

@ -6,15 +6,15 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.flow.Flow 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( data class Subscription(
@PrimaryKey val id: Long, // Internal ID, only used in Repository and activities @PrimaryKey val id: Long, // Internal ID, only used in Repository and activities
@ColumnInfo(name = "baseUrl") val baseUrl: String, @ColumnInfo(name = "baseUrl") val baseUrl: String,
@ColumnInfo(name = "topic") val topic: String, @ColumnInfo(name = "topic") val topic: String,
@ColumnInfo(name = "instant") val instant: Boolean, @ColumnInfo(name = "instant") val instant: Boolean,
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule @ColumnInfo(name = "mutedUntil") val mutedUntil: Long, // TODO notificationSound, notificationSchedule
@ColumnInfo(name = "upAppId") val upAppId: String, @ColumnInfo(name = "upAppId") val upAppId: String?,
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String, @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?,
@Ignore val totalCount: Int = 0, // Total notifications @Ignore val totalCount: Int = 0, // Total notifications
@Ignore val newCount: Int = 0, // New notifications @Ignore val newCount: Int = 0, // New notifications
@Ignore val lastActive: Long = 0, // Unix timestamp @Ignore val lastActive: Long = 0, // Unix timestamp
@ -110,8 +110,8 @@ abstract class Database : RoomDatabase() {
private val MIGRATION_4_5 = object : Migration(3, 4) { private val MIGRATION_4_5 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE Subscription ADD COLUMN upAppId TEXT NOT NULL DEFAULT('')") db.execSQL("ALTER TABLE Subscription ADD COLUMN upAppId TEXT")
db.execSQL("ALTER TABLE Subscription ADD COLUMN upConnectorToken TEXT NOT NULL DEFAULT('')") db.execSQL("ALTER TABLE Subscription ADD COLUMN upConnectorToken TEXT")
} }
} }
} }
@ -166,11 +166,24 @@ interface SubscriptionDao {
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive
FROM Subscription AS s FROM Subscription AS s
LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 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 GROUP BY s.id
""") """)
fun get(subscriptionId: Long): SubscriptionWithMetadata? 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 @Insert
fun add(subscription: Subscription) fun add(subscription: Subscription)

View file

@ -54,6 +54,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
return toSubscription(subscriptionDao.get(baseUrl, topic)) return toSubscription(subscriptionDao.get(baseUrl, topic))
} }
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun getSubscriptionByConnectorToken(connectorToken: String): Subscription? {
return toSubscription(subscriptionDao.getByConnectorToken(connectorToken))
}
@Suppress("RedundantSuspendModifier") @Suppress("RedundantSuspendModifier")
@WorkerThread @WorkerThread
suspend fun addSubscription(subscription: Subscription) { suspend fun addSubscription(subscription: Subscription) {

View file

@ -5,6 +5,7 @@ import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.up.Distributor import io.heckel.ntfy.up.Distributor
import io.heckel.ntfy.util.safeLet
class NotificationDispatcher(val context: Context, val repository: Repository) { class NotificationDispatcher(val context: Context, val repository: Repository) {
private val notifier = NotificationService(context) private val notifier = NotificationService(context)
@ -18,8 +19,8 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
fun dispatch(subscription: Subscription, notification: Notification) { fun dispatch(subscription: Subscription, notification: Notification) {
val muted = checkMuted(subscription) val muted = checkMuted(subscription)
val notify = checkNotify(subscription, notification, muted) val notify = checkNotify(subscription, notification, muted)
val broadcast = subscription.upAppId == "" val broadcast = subscription.upAppId == null
val distribute = subscription.upAppId != "" val distribute = subscription.upAppId != null
if (notify) { if (notify) {
notifier.send(subscription, notification) notifier.send(subscription, notification)
} }
@ -27,7 +28,9 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
broadcaster.send(subscription, notification, muted) broadcaster.send(subscription, notification, muted)
} }
if (distribute) { if (distribute) {
distributor.sendMessage(subscription.upAppId, subscription.upConnectorToken, notification.message) safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
distributor.sendMessage(appId, connectorToken, notification.message)
}
} }
} }

View file

@ -7,6 +7,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.ui.SubscriberManager 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.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -24,44 +26,62 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
Log.w(TAG, "Trying to register an app without packageName") Log.w(TAG, "Trying to register an app without packageName")
return return
} }
val baseUrl = context!!.getString(R.string.app_base_url) // FIXME 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 app = context!!.applicationContext as Application
val repository = app.repository val repository = app.repository
val distributor = Distributor(app) 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) { 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) repository.addSubscription(subscription)
val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus() val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus()
val subscriberManager = SubscriberManager(context!!) val subscriberManager = SubscriberManager(app)
subscriberManager.refreshService(subscriptionIdsWithInstantStatus) subscriberManager.refreshService(subscriptionIdsWithInstantStatus)
distributor.sendEndpoint(appId, connectorToken, endpoint)
} }
distributor.sendEndpoint(appId, connectorToken)
// XXXXXXXXX
} }
ACTION_UNREGISTER -> { ACTION_UNREGISTER -> {
val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: "" val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: ""
Log.d(TAG, "Unregister: connectorToken=$connectorToken") Log.d(TAG, "Unregister: connectorToken=$connectorToken")
// XXXXXXX val app = context!!.applicationContext as Application
val distributor = Distributor(context!!) val repository = app.repository
distributor.sendUnregistered("org.unifiedpush.example", connectorToken) 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 { companion object {
private const val TAG = "NtfyUpBroadcastRecv" private const val TAG = "NtfyUpBroadcastRecv"
private const val TOPIC_LENGTH = 16
} }
} }

View file

@ -2,35 +2,39 @@ package io.heckel.ntfy.up
import android.content.Context import android.content.Context
import android.content.Intent 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) { class Distributor(val context: Context) {
fun sendMessage(app: String, token: String, message: String) { fun sendMessage(app: String, connectorToken: String, message: String) {
val broadcastIntent = Intent() val broadcastIntent = Intent()
broadcastIntent.`package` = app broadcastIntent.`package` = app
broadcastIntent.action = ACTION_MESSAGE broadcastIntent.action = ACTION_MESSAGE
broadcastIntent.putExtra(EXTRA_TOKEN, token) broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
broadcastIntent.putExtra(EXTRA_MESSAGE, message) broadcastIntent.putExtra(EXTRA_MESSAGE, message)
context.sendBroadcast(broadcastIntent) context.sendBroadcast(broadcastIntent)
} }
fun sendEndpoint(app: String, token: String) { fun sendEndpoint(app: String, connectorToken: String, endpoint: String) {
val appBaseUrl = context.getString(R.string.app_base_url) // FIXME
val broadcastIntent = Intent() val broadcastIntent = Intent()
broadcastIntent.`package` = app broadcastIntent.`package` = app
broadcastIntent.action = ACTION_NEW_ENDPOINT broadcastIntent.action = ACTION_NEW_ENDPOINT
broadcastIntent.putExtra(EXTRA_TOKEN, token) broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
broadcastIntent.putExtra(EXTRA_ENDPOINT, topicUrlUp(appBaseUrl, token)) broadcastIntent.putExtra(EXTRA_ENDPOINT, endpoint)
context.sendBroadcast(broadcastIntent) context.sendBroadcast(broadcastIntent)
} }
fun sendUnregistered(app: String, token: String) { fun sendUnregistered(app: String, connectorToken: String) {
val broadcastIntent = Intent() val broadcastIntent = Intent()
broadcastIntent.`package` = app broadcastIntent.`package` = app
broadcastIntent.action = ACTION_UNREGISTERED 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) context.sendBroadcast(broadcastIntent)
} }
} }

View file

@ -5,6 +5,7 @@ import android.animation.ValueAnimator
import android.view.Window import android.view.Window
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import java.security.SecureRandom
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -102,3 +103,14 @@ fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
} }
statusBarColorAnimation.start() 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 <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
return if (p1 != null && p2 != null) block(p1, p2) else null
}