Send messages via intent; Broadcast received messages

This commit is contained in:
Philipp Heckel 2021-12-11 15:09:07 -05:00
parent c163e6e96e
commit 323e013391
10 changed files with 144 additions and 23 deletions

View file

@ -3,6 +3,7 @@
package="io.heckel.ntfy">
<!--
Permissions
- INTERNET is needed because we need to talk to the ntfy server(s)
- FOREGROUND_SERVICE is needed to support "use another server" feature
- WAKE_LOCK & RECEIVE_BOOT_COMPLETED are required to restart the foreground service
if it is stopped; see https://robertohuertas.com/2019/06/29/android_foreground_services/
@ -56,14 +57,20 @@
<service android:name=".msg.SubscriberService"/>
<!-- Subscriber service restart on reboot -->
<receiver
android:name=".msg.SubscriberService$StartReceiver"
android:enabled="true">
<receiver android:name=".msg.SubscriberService$StartReceiver" android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<!-- Broadcast receiver to send messages via intents -->
<receiver android:name=".msg.BroadcastService$BroadcastReceiver" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="io.heckel.ntfy.SEND_MESSAGE"/>
</intent-filter>
</receiver>
<!-- Firebase messaging (note that this is empty in the F-Droid flavor) -->
<service
android:name=".firebase.FirebaseService"

View file

@ -85,19 +85,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun addNotification(notification: Notification): Boolean {
suspend fun addNotification(notification: Notification): NotificationAddResult {
val maybeExistingNotification = notificationDao.get(notification.id)
if (maybeExistingNotification == null) {
notificationDao.add(notification)
return shouldNotify(notification)
val detailsVisible = detailViewSubscriptionId.get() == notification.subscriptionId
val muted = isMuted(notification.subscriptionId)
val notify = !detailsVisible && !muted
return NotificationAddResult(notify = notify, broadcast = true, muted = muted)
}
return false
}
private suspend fun shouldNotify(notification: Notification): Boolean {
val detailViewOpen = detailViewSubscriptionId.get() == notification.subscriptionId
val muted = isMuted(notification.subscriptionId)
return !detailViewOpen && !muted
return NotificationAddResult(notify = false, broadcast = false, muted = false)
}
@Suppress("RedundantSuspendModifier")
@ -217,6 +214,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri
return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE }
}
data class NotificationAddResult(
val notify: Boolean,
val broadcast: Boolean,
val muted: Boolean,
)
companion object {
const val SHARED_PREFS_ID = "MainPreferences"
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"

View file

@ -28,20 +28,25 @@ class ApiService {
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
.build()
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>) {
fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List<String>, delay: String) {
val url = topicUrl(baseUrl, topic)
Log.d(TAG, "Publishing to $url")
var builder = Request.Builder()
.url(url)
.addHeader("X-Priority", priority.toString())
.put(message.toRequestBody())
if (priority in 1..5) {
builder = builder.addHeader("X-Priority", priority.toString())
}
if (tags.isNotEmpty()) {
builder = builder.addHeader("X-Tags", tags.joinToString(","))
}
if (title.isNotEmpty()) {
builder = builder.addHeader("X-Title", title)
}
if (delay.isNotEmpty()) {
builder = builder.addHeader("X-Delay", delay)
}
client.newCall(builder.build()).execute().use { response ->
if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when publishing to $url")

View file

@ -0,0 +1,80 @@
package io.heckel.ntfy.msg
import android.content.Context
import android.content.Intent
import android.util.Log
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.util.joinTagsMap
import io.heckel.ntfy.util.splitTags
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class BroadcastService(private val ctx: Context) {
fun send(subscription: Subscription, notification: Notification, muted: Boolean) {
val intent = Intent()
intent.action = MESSAGE_RECEIVED_ACTION
intent.putExtra("id", notification.id)
intent.putExtra("base_url", subscription.baseUrl)
intent.putExtra("topic", subscription.topic)
intent.putExtra("title", notification.title)
intent.putExtra("message", notification.message)
intent.putExtra("tags", notification.tags)
intent.putExtra("tags_map", joinTagsMap(splitTags(notification.tags)))
intent.putExtra("priority", notification.priority)
intent.putExtra("muted", muted)
Log.d(TAG, "Sending intent broadcast: $intent")
ctx.sendBroadcast(intent)
}
class BroadcastReceiver : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Broadcast received: $intent")
when (intent.action) {
MESSAGE_SEND_ACTION -> send(context, intent)
}
}
private fun send(ctx: Context, intent: Intent) {
val api = ApiService()
val topic = intent.getStringExtra("topic") ?: return
val message = intent.getStringExtra("message") ?: return
val baseUrl = intent.getStringExtra("base_url") ?: ctx.getString(R.string.app_base_url)
val title = intent.getStringExtra("title") ?: ""
val tags = intent.getStringExtra("tags") ?: ""
val priority = if (intent.getStringExtra("priority") != null) {
when (intent.getStringExtra("priority")) {
"min", "1" -> 1
"low", "2" -> 2
"default", "3" -> 3
"high", "4" -> 4
"urgent", "max", "5" -> 5
else -> 0
}
} else {
intent.getIntExtra("priority", 0)
}
val delay = intent.getStringExtra("delay") ?: ""
GlobalScope.launch(Dispatchers.IO) {
api.publish(
baseUrl = baseUrl,
topic = topic,
message = message,
title = title,
priority = priority,
tags = splitTags(tags),
delay = delay
)
}
}
}
companion object {
private const val TAG = "NtfyBroadcastService"
private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED"
private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE" // If changed, change in manifest too!
}
}

View file

@ -33,6 +33,7 @@ class SubscriberService : Service() {
private val connections = ConcurrentHashMap<String, SubscriberConnection>() // Base URL -> Connection
private val api = ApiService()
private val notifier = NotificationService(this)
private val broadcaster = BroadcastService(this)
private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null
@ -175,11 +176,15 @@ class SubscriberService : Service() {
val url = topicUrl(subscription.baseUrl, subscription.topic)
Log.d(TAG, "[$url] Received notification: $n")
GlobalScope.launch(Dispatchers.IO) {
val shouldNotify = repository.addNotification(n)
if (shouldNotify) {
val result = repository.addNotification(n)
if (result.notify) {
Log.d(TAG, "[$url] Showing notification: $n")
notifier.send(subscription, n)
}
if (result.broadcast) {
Log.d(TAG, "[$url] Broadcasting notification: $n")
broadcaster.send(subscription, n, result.muted)
}
}
}

View file

@ -334,7 +334,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
val tags = possibleTags.shuffled().take(Random.nextInt(0, 4))
val title = if (Random.nextBoolean()) getString(R.string.detail_test_title) else ""
val message = getString(R.string.detail_test_message, priority)
api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags)
api.publish(subscriptionBaseUrl, subscriptionTopic, message, title, priority, tags, delay = "")
} catch (e: Exception) {
runOnUiThread {
Toast

View file

@ -27,6 +27,7 @@ import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.work.PollWorker
import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.util.fadeStatusBarColor
import io.heckel.ntfy.util.formatDateShort
import kotlinx.coroutines.Dispatchers
@ -56,6 +57,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private var actionMode: ActionMode? = null
private var workManager: WorkManager? = 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 appBaseUrl: String? = null // Context-dependent
@ -68,6 +70,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Dependencies that depend on Context
workManager = WorkManager.getInstance(this)
notifier = NotificationService(this)
broadcaster = BroadcastService(this)
subscriberManager = SubscriberManager(this)
appBaseUrl = getString(R.string.app_base_url)
@ -315,10 +318,13 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
newNotifications.forEach { notification ->
newNotificationsCount++
val notificationWithId = notification.copy(notificationId = Random.nextInt())
val shouldNotify = repository.addNotification(notificationWithId)
if (shouldNotify) {
val result = repository.addNotification(notificationWithId)
if (result.notify) {
notifier?.send(subscription, notificationWithId)
}
if (result.broadcast) {
broadcaster?.send(subscription, notification, result.muted)
}
}
}
val toastMessage = if (newNotificationsCount == 0) {

View file

@ -30,6 +30,10 @@ fun joinTags(tags: List<String>?): String {
return tags?.joinToString(",") ?: ""
}
fun joinTagsMap(tags: List<String>?): String {
return tags?.mapIndexed { i, tag -> { "${i+1}=${tag}" }}?.joinToString(",") ?: ""
}
fun splitTags(tags: String?): List<String> {
return if (tags == null || tags == "") {
emptyList()

View file

@ -8,6 +8,7 @@ import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -25,6 +26,7 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
val sharedPrefs = applicationContext.getSharedPreferences(Repository.SHARED_PREFS_ID, Context.MODE_PRIVATE)
val repository = Repository.getInstance(sharedPrefs, database.subscriptionDao(), database.notificationDao())
val notifier = NotificationService(applicationContext)
val broadcaster = BroadcastService(applicationContext)
val api = ApiService()
try {
@ -34,10 +36,13 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
.onlyNewNotifications(subscription.id, notifications)
.map { it.copy(notificationId = Random.nextInt()) }
newNotifications.forEach { notification ->
val shouldNotify = repository.addNotification(notification)
if (shouldNotify) {
val result = repository.addNotification(notification)
if (result.notify) {
notifier.send(subscription, notification)
}
if (result.broadcast) {
broadcaster.send(subscription, notification, result.muted)
}
}
}
Log.d(TAG, "Finished polling for new notifications")

View file

@ -6,6 +6,7 @@ import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.msg.BroadcastService
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.toPriority
import kotlinx.coroutines.CoroutineScope
@ -17,6 +18,7 @@ class FirebaseService : FirebaseMessagingService() {
private val repository by lazy { (application as Application).repository }
private val job = SupervisorJob()
private val notifier = NotificationService(this)
private val broadcaster = BroadcastService(this)
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// We only process data messages
@ -56,13 +58,17 @@ class FirebaseService : FirebaseMessagingService() {
tags = tags ?: "",
deleted = false
)
val shouldNotify = repository.addNotification(notification)
val result = repository.addNotification(notification)
// Send notification (only if it's not already known)
if (shouldNotify) {
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)
}
}
}