Send messages via intent; Broadcast received messages
This commit is contained in:
parent
c163e6e96e
commit
323e013391
10 changed files with 144 additions and 23 deletions
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun shouldNotify(notification: Notification): Boolean {
|
||||
val detailViewOpen = detailViewSubscriptionId.get() == notification.subscriptionId
|
||||
val detailsVisible = detailViewSubscriptionId.get() == notification.subscriptionId
|
||||
val muted = isMuted(notification.subscriptionId)
|
||||
return !detailViewOpen && !muted
|
||||
val notify = !detailsVisible && !muted
|
||||
return NotificationAddResult(notify = notify, broadcast = true, muted = 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"
|
||||
|
|
|
@ -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")
|
||||
|
|
80
app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt
Normal file
80
app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt
Normal 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!
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue