Full end to end use case works; still ugly though
This commit is contained in:
parent
7dbbf12c99
commit
73f610afa8
7 changed files with 111 additions and 45 deletions
|
@ -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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue