Forward messages, don't show ntfy notification

This commit is contained in:
Philipp Heckel 2021-12-29 21:36:47 +01:00
parent 94e595110d
commit 7e9da28704
9 changed files with 104 additions and 119 deletions

View file

@ -85,16 +85,13 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
@Suppress("RedundantSuspendModifier") @Suppress("RedundantSuspendModifier")
@WorkerThread @WorkerThread
suspend fun addNotification(notification: Notification): NotificationAddResult { suspend fun addNotification(notification: Notification): Boolean {
val maybeExistingNotification = notificationDao.get(notification.id) val maybeExistingNotification = notificationDao.get(notification.id)
if (maybeExistingNotification == null) { if (maybeExistingNotification != null) {
notificationDao.add(notification) return false
val detailsVisible = detailViewSubscriptionId.get() == notification.subscriptionId
val muted = isMuted(notification.subscriptionId)
val notify = !detailsVisible && !muted
return NotificationAddResult(notification = notification, notify = notify, broadcast = true, muted = muted)
} }
return NotificationAddResult(notification = notification, notify = false, broadcast = false, forward = false, muted = false) notificationDao.add(notification)
return true
} }
@Suppress("RedundantSuspendModifier") @Suppress("RedundantSuspendModifier")
@ -141,7 +138,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
return s.mutedUntil == 1L || (s.mutedUntil > 1L && s.mutedUntil > System.currentTimeMillis()/1000) return s.mutedUntil == 1L || (s.mutedUntil > 1L && s.mutedUntil > System.currentTimeMillis()/1000)
} }
private fun isGlobalMuted(): Boolean { fun isGlobalMuted(): Boolean {
val mutedUntil = getGlobalMutedUntil() val mutedUntil = getGlobalMutedUntil()
return mutedUntil == 1L || (mutedUntil > 1L && mutedUntil > System.currentTimeMillis()/1000) return mutedUntil == 1L || (mutedUntil > 1L && mutedUntil > System.currentTimeMillis()/1000)
} }
@ -228,14 +225,6 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE } return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE }
} }
data class NotificationAddResult(
val notification: Notification,
val notify: Boolean,
val broadcast: Boolean,
val forward: Boolean, // Forward to UnifiedPush connector
val muted: Boolean,
)
companion object { companion object {
const val SHARED_PREFS_ID = "MainPreferences" const val SHARED_PREFS_ID = "MainPreferences"
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion" const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"

View file

@ -1,28 +1,49 @@
package io.heckel.ntfy.msg package io.heckel.ntfy.msg
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.ui.DetailActivity import io.heckel.ntfy.up.Distributor
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.formatMessage class NotificationDispatcher(val context: Context, val repository: Repository) {
import io.heckel.ntfy.util.formatTitle private val notifier = NotificationService(context)
private val broadcaster = BroadcastService(context)
private val distributor = Distributor(context)
fun init() {
notifier.createNotificationChannels()
}
class NotificationDispatcher(val context: Context) {
fun dispatch(subscription: Subscription, notification: Notification) { fun dispatch(subscription: Subscription, notification: Notification) {
val muted = checkMuted(subscription)
val notify = checkNotify(subscription, notification, muted)
val broadcast = subscription.upAppId == ""
val distribute = subscription.upAppId != ""
if (notify) {
notifier.send(subscription, notification)
}
if (broadcast) {
broadcaster.send(subscription, notification, muted)
}
if (distribute) {
distributor.sendMessage(subscription.upAppId, subscription.upConnectorToken, notification.message)
}
}
private fun checkNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean {
if (subscription.upAppId != "") {
return false
}
val detailsVisible = repository.detailViewSubscriptionId.get() == notification.subscriptionId
return !detailsVisible && !muted
}
private fun checkMuted(subscription: Subscription): Boolean {
if (repository.isGlobalMuted()) {
return true
}
return subscription.mutedUntil == 1L || (subscription.mutedUntil > 1L && subscription.mutedUntil > System.currentTimeMillis()/1000)
} }
companion object { companion object {

View file

@ -58,10 +58,9 @@ class SubscriberService : Service() {
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
private val repository by lazy { (application as Application).repository } private val repository by lazy { (application as Application).repository }
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
private val connections = ConcurrentHashMap<String, SubscriberConnection>() // Base URL -> Connection private val connections = ConcurrentHashMap<String, SubscriberConnection>() // Base URL -> Connection
private val api = ApiService() private val api = ApiService()
private val notifier = NotificationService(this)
private val broadcaster = BroadcastService(this)
private var notificationManager: NotificationManager? = null private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null private var serviceNotification: Notification? = null
@ -201,18 +200,13 @@ class SubscriberService : Service() {
repository.updateState(subscriptionIds, state) repository.updateState(subscriptionIds, state)
} }
private fun onNotificationReceived(subscription: Subscription, n: io.heckel.ntfy.data.Notification) { private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.data.Notification) {
val url = topicUrl(subscription.baseUrl, subscription.topic) val url = topicUrl(subscription.baseUrl, subscription.topic)
Log.d(TAG, "[$url] Received notification: $n") Log.d(TAG, "[$url] Received notification: $notification")
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val result = repository.addNotification(n) if (repository.addNotification(notification)) {
if (result.notify) { Log.d(TAG, "[$url] Dispatching notification $notification")
Log.d(TAG, "[$url] Showing notification: $n") dispatcher.dispatch(subscription, notification)
notifier.send(subscription, n)
}
if (result.broadcast) {
Log.d(TAG, "[$url] Broadcasting notification: $n")
broadcaster.send(subscription, n, result.muted)
} }
} }
} }

View file

@ -52,8 +52,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var workManager: WorkManager? = null // Context-dependent private var workManager: WorkManager? = null // Context-dependent
private var dispatcher: NotificationDispatcher? = null // Context-dependent private var dispatcher: NotificationDispatcher? = null // Context-dependent
private var notifier: NotificationService? = null // Context-dependent
private var broadcaster: BroadcastService? = null // Context-dependent
private var subscriberManager: SubscriberManager? = null // Context-dependent private var subscriberManager: SubscriberManager? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent private var appBaseUrl: String? = null // Context-dependent
@ -65,9 +63,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Dependencies that depend on Context // Dependencies that depend on Context
workManager = WorkManager.getInstance(this) workManager = WorkManager.getInstance(this)
dispatcher = NotificationDispatcher(this) dispatcher = NotificationDispatcher(this, repository)
notifier = NotificationService(this)
broadcaster = BroadcastService(this)
subscriberManager = SubscriberManager(this) subscriberManager = SubscriberManager(this)
appBaseUrl = getString(R.string.app_base_url) appBaseUrl = getString(R.string.app_base_url)
@ -113,7 +109,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
} }
// Create notification channels right away, so we can configure them immediately after installing the app // Create notification channels right away, so we can configure them immediately after installing the app
notifier!!.createNotificationChannels() dispatcher?.init()
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies) // Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
messenger.subscribe(ApiService.CONTROL_TOPIC) messenger.subscribe(ApiService.CONTROL_TOPIC)
@ -342,13 +338,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
newNotifications.forEach { notification -> newNotifications.forEach { notification ->
newNotificationsCount++ newNotificationsCount++
val notificationWithId = notification.copy(notificationId = Random.nextInt()) val notificationWithId = notification.copy(notificationId = Random.nextInt())
val result = repository.addNotification(notificationWithId) if (repository.addNotification(notificationWithId)) {
dispatcher?.dispatch() dispatcher?.dispatch(subscription, notificationWithId)
if (result.notify) {
notifier?.send(subscription, notificationWithId)
}
if (result.broadcast) {
broadcaster?.send(subscription, notification, result.muted)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -28,6 +28,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
val topic = connectorToken // FIXME val topic = connectorToken // FIXME
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 subscription = Subscription( val subscription = Subscription(
id = Random.nextLong(), id = Random.nextLong(),
baseUrl = baseUrl, baseUrl = baseUrl,
@ -44,14 +45,15 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
repository.addSubscription(subscription) repository.addSubscription(subscription)
} }
sendEndpoint(context!!, appId, connectorToken) distributor.sendEndpoint(appId, connectorToken)
// XXXXXXXXX // 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 // XXXXXXX
sendUnregistered(context!!, "org.unifiedpush.example", connectorToken) val distributor = Distributor(context!!)
distributor.sendUnregistered("org.unifiedpush.example", connectorToken)
} }
} }
} }

View file

@ -0,0 +1,36 @@
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) {
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(app: String, token: String) {
val appBaseUrl = context.getString(R.string.app_base_url) // FIXME
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(app: String, token: String) {
val broadcastIntent = Intent()
broadcastIntent.`package` = app
broadcastIntent.action = ACTION_UNREGISTERED
broadcastIntent.putExtra(EXTRA_TOKEN, token)
context.sendBroadcast(broadcastIntent)
}
}

View file

@ -1,34 +0,0 @@
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)
}

View file

@ -7,8 +7,10 @@ import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Database import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.firebase.FirebaseService
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.BroadcastService import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationDispatcher
import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.msg.NotificationService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -25,8 +27,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
val database = Database.getInstance(applicationContext) val database = Database.getInstance(applicationContext)
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE) val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
val repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao()) val repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
val notifier = NotificationService(applicationContext) val dispatcher = NotificationDispatcher(applicationContext, repository)
val broadcaster = BroadcastService(applicationContext)
val api = ApiService() val api = ApiService()
repository.getSubscriptions().forEach{ subscription -> repository.getSubscriptions().forEach{ subscription ->
@ -36,12 +37,8 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
.onlyNewNotifications(subscription.id, notifications) .onlyNewNotifications(subscription.id, notifications)
.map { it.copy(notificationId = Random.nextInt()) } .map { it.copy(notificationId = Random.nextInt()) }
newNotifications.forEach { notification -> newNotifications.forEach { notification ->
val result = repository.addNotification(notification) if (repository.addNotification(notification)) {
if (result.notify) { dispatcher.dispatch(subscription, notification)
notifier.send(subscription, notification)
}
if (result.broadcast) {
broadcaster.send(subscription, notification, result.muted)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -7,10 +7,7 @@ import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.*
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.SubscriberService
import io.heckel.ntfy.util.toPriority import io.heckel.ntfy.util.toPriority
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -19,9 +16,8 @@ import kotlin.random.Random
class FirebaseService : FirebaseMessagingService() { class FirebaseService : FirebaseMessagingService() {
private val repository by lazy { (application as Application).repository } private val repository by lazy { (application as Application).repository }
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
private val job = SupervisorJob() private val job = SupervisorJob()
private val notifier = NotificationService(this)
private val broadcaster = BroadcastService(this)
private val messenger = FirebaseMessenger() private val messenger = FirebaseMessenger()
override fun onMessageReceived(remoteMessage: RemoteMessage) { override fun onMessageReceived(remoteMessage: RemoteMessage) {
@ -81,16 +77,9 @@ class FirebaseService : FirebaseMessagingService() {
tags = tags ?: "", tags = tags ?: "",
deleted = false deleted = false
) )
val result = repository.addNotification(notification) if (repository.addNotification(notification)) {
Log.d(TAG, "Dispatching notification for message: from=${remoteMessage.from}, data=${data}")
// Send notification (only if it's not already known) dispatcher.dispatch(subscription, notification)
if (result.notify) {
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
notifier.send(subscription, notification)
}
if (result.broadcast) {
Log.d(TAG, "Sending broadcast for message: from=${remoteMessage.from}, data=${data}")
broadcaster.send(subscription, notification, result.muted)
} }
} }
} }