diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9210db0..8c808ad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="io.heckel.ntfy"> - + + + + + + + + + ) { + fun publish(baseUrl: String, topic: String, message: String, title: String, priority: Int, tags: List, 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") diff --git a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt new file mode 100644 index 0000000..a45b4a4 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt @@ -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! + } +} diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt index 07b19e3..2307a20 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt @@ -33,6 +33,7 @@ class SubscriberService : Service() { private val connections = ConcurrentHashMap() // 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) + } } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 2ae156d..2e29981 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -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 diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 234f652..fe5e7bf 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 50689c0..11598c1 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -30,6 +30,10 @@ fun joinTags(tags: List?): String { return tags?.joinToString(",") ?: "" } +fun joinTagsMap(tags: List?): String { + return tags?.mapIndexed { i, tag -> { "${i+1}=${tag}" }}?.joinToString(",") ?: "" +} + fun splitTags(tags: String?): List { return if (tags == null || tags == "") { emptyList() diff --git a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt index 7ebbbf5..4c812df 100644 --- a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt @@ -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") diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt index 815741d..2dd4590 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -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) + } } }