From 4efdce54efc03a772e0787d8b8f3f2d8b7d8d31f Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Thu, 30 Dec 2021 14:23:47 +0100
Subject: [PATCH] Works and is not super ugly

---
 .../main/java/io/heckel/ntfy/data/Database.kt |   4 +-
 .../java/io/heckel/ntfy/ui/MainActivity.kt    |  16 +-
 .../java/io/heckel/ntfy/ui/MainAdapter.kt     |  10 +-
 .../java/io/heckel/ntfy/ui/MainViewModel.kt   |   9 +-
 .../io/heckel/ntfy/up/BroadcastReceiver.kt    | 149 +++++++++++-------
 .../java/io/heckel/ntfy/up/Distributor.kt     |   9 ++
 app/src/main/res/values/strings.xml           |   5 +-
 7 files changed, 130 insertions(+), 72 deletions(-)

diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt
index ad87a96..e0187ce 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Database.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt
@@ -34,8 +34,8 @@ data class SubscriptionWithMetadata(
     val topic: String,
     val instant: Boolean,
     val mutedUntil: Long,
-    val upAppId: String,
-    val upConnectorToken: String,
+    val upAppId: String?,
+    val upConnectorToken: String?,
     val totalCount: Int,
     val newCount: Int,
     val lastActive: Long
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 3a3e813..01222fa 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -283,8 +283,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
             topic = topic,
             instant = instant,
             mutedUntil = 0,
-            upAppId = "",
-            upConnectorToken = "",
+            upAppId = null,
+            upConnectorToken = null,
             totalCount = 0,
             newCount = 0,
             lastActive = Date().time/1000
@@ -314,11 +314,21 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
     private fun onSubscriptionItemClick(subscription: Subscription) {
         if (actionMode != null) {
             handleActionModeClick(subscription)
+        } else if (subscription.upAppId != null) { // Not UnifiedPush
+            displayUnifiedPushToast(subscription)
         } else {
             startDetailView(subscription)
         }
     }
 
+    private fun displayUnifiedPushToast(subscription: Subscription) {
+        runOnUiThread {
+            val appId = subscription.upAppId ?: ""
+            val toastMessage = getString(R.string.main_unified_push_toast, appId)
+            Toast.makeText(this@MainActivity, toastMessage, Toast.LENGTH_LONG).show()
+        }
+    }
+
     private fun onSubscriptionItemLongClick(subscription: Subscription) {
         if (actionMode == null) {
             beginActionMode(subscription)
@@ -415,7 +425,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
         val dialog = builder
             .setMessage(R.string.main_action_mode_delete_dialog_message)
             .setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ ->
-                adapter.selected.map { viewModel.remove(it) }
+                adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) }
                 finishActionMode()
             }
             .setNegativeButton(R.string.main_action_mode_delete_dialog_cancel) { _, _ ->
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
index adf52a5..dfbc016 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
@@ -55,7 +55,9 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
 
         fun bind(subscription: Subscription) {
             this.subscription = subscription
-            var statusMessage = if (subscription.totalCount == 1) {
+            var statusMessage = if (subscription.upAppId != null) {
+                context.getString(R.string.main_item_status_unified_push, subscription.upAppId)
+            } else if (subscription.totalCount == 1) {
                 context.getString(R.string.main_item_status_text_one, subscription.totalCount)
             } else {
                 context.getString(R.string.main_item_status_text_not_one, subscription.totalCount)
@@ -82,11 +84,11 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon
             notificationDisabledUntilImageView.visibility = if (subscription.mutedUntil > 1L) View.VISIBLE else View.GONE
             notificationDisabledForeverImageView.visibility = if (subscription.mutedUntil == 1L) View.VISIBLE else View.GONE
             instantImageView.visibility = if (subscription.instant) View.VISIBLE else View.GONE
-            if (subscription.newCount > 0) {
+            if (subscription.upAppId != null || subscription.newCount == 0) {
+                newItemsView.visibility = View.GONE
+            } else {
                 newItemsView.visibility = View.VISIBLE
                 newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+"
-            } else {
-                newItemsView.visibility = View.GONE
             }
             itemView.setOnClickListener { onClick(subscription) }
             itemView.setOnLongClickListener { onLongClick(subscription); true }
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt b/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
index f0b0275..9c24520 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainViewModel.kt
@@ -1,10 +1,12 @@
 package io.heckel.ntfy.ui
 
+import android.content.Context
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
 import io.heckel.ntfy.data.*
+import io.heckel.ntfy.up.Distributor
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlin.collections.List
@@ -22,7 +24,12 @@ class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
         repository.addSubscription(subscription)
     }
 
-    fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) {
+    fun remove(context: Context, subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) {
+        val subscription = repository.getSubscription(subscriptionId) ?: return@launch
+        if (subscription.upAppId != null && subscription.upConnectorToken != null) {
+            val distributor = Distributor(context)
+            distributor.sendUnregistered(subscription.upAppId, subscription.upConnectorToken)
+        }
         repository.removeAllNotifications(subscriptionId)
         repository.removeSubscription(subscriptionId)
     }
diff --git a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
index 125c766..74fba12 100644
--- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
+++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
@@ -5,6 +5,7 @@ import android.content.Intent
 import android.util.Log
 import io.heckel.ntfy.R
 import io.heckel.ntfy.app.Application
+import io.heckel.ntfy.data.Repository
 import io.heckel.ntfy.data.Subscription
 import io.heckel.ntfy.ui.SubscriberManager
 import io.heckel.ntfy.util.randomString
@@ -17,71 +18,99 @@ import kotlin.random.Random
 
 class BroadcastReceiver : android.content.BroadcastReceiver() {
     override fun onReceive(context: Context?, intent: Intent?) {
-        when (intent!!.action) {
-            ACTION_REGISTER -> {
-                val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: ""
-                val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: ""
-                Log.d(TAG, "Register: app=$appId, connectorToken=$connectorToken")
-                if (appId.isBlank()) {
-                    Log.w(TAG, "Trying to register an app without packageName")
-                    return
-                }
-                val baseUrl = context!!.getString(R.string.app_base_url) // FIXME
-                val topic = "up" + randomString(TOPIC_LENGTH)
-                val endpoint = topicUrlUp(baseUrl, topic)
-                val app = context!!.applicationContext as Application
-                val repository = app.repository
-                val distributor = Distributor(app)
-                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)
-                    val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus()
-                    val subscriberManager = SubscriberManager(app)
-                    subscriberManager.refreshService(subscriptionIdsWithInstantStatus)
-                    distributor.sendEndpoint(appId, connectorToken, endpoint)
-                }
-            }
-            ACTION_UNREGISTER -> {
-                val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: ""
-                Log.d(TAG, "Unregister: connectorToken=$connectorToken")
-                val app = context!!.applicationContext as Application
-                val repository = app.repository
-                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)
-                    }
-                }
-            }
+        if (context == null || intent == null) {
+            return
         }
+        when (intent.action) {
+            ACTION_REGISTER -> register(context, intent)
+            ACTION_UNREGISTER -> unregister(context, intent)
+        }
+    }
+
+    private fun register(context: Context, intent: Intent) {
+        val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: return
+        val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return
+        val app = context.applicationContext as Application
+        val repository = app.repository
+        val distributor = Distributor(app)
+        Log.d(TAG, "REGISTER received for app $appId (connectorToken=$connectorToken)")
+        if (appId.isBlank()) {
+            Log.w(TAG, "Refusing registration: empty application")
+            distributor.sendRegistrationRefused(appId, connectorToken)
+            return
+        }
+        GlobalScope.launch(Dispatchers.IO) {
+            val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken)
+            if (existingSubscription != null) {
+                if (existingSubscription.upAppId == appId) {
+                    val endpoint = topicUrlUp(existingSubscription.baseUrl, existingSubscription.topic)
+                    Log.d(TAG, "Subscription with connectorToken $connectorToken exists. Sending endpoint $endpoint.")
+                    distributor.sendEndpoint(appId, connectorToken, endpoint)
+                } else {
+                    Log.d(TAG, "Subscription with connectorToken $connectorToken exists for a different app. Refusing registration.")
+                    distributor.sendRegistrationRefused(appId, connectorToken)
+                }
+                return@launch
+            }
+            val baseUrl = context.getString(R.string.app_base_url) // FIXME
+            val topic = UP_PREFIX + randomString(TOPIC_LENGTH)
+            val endpoint = topicUrlUp(baseUrl, topic)
+            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
+            )
+
+            // Add subscription
+            Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
+            repository.addSubscription(subscription)
+            distributor.sendEndpoint(appId, connectorToken, endpoint)
+
+            // Refresh (and maybe start) foreground service
+            refreshSubscriberService(app, repository)
+        }
+    }
+
+    private fun unregister(context: Context, intent: Intent) {
+        val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return
+        val app = context.applicationContext as Application
+        val repository = app.repository
+        val distributor = Distributor(app)
+        Log.d(TAG, "UNREGISTER received (connectorToken=$connectorToken)")
+        GlobalScope.launch(Dispatchers.IO) {
+            val existingSubscription = repository.getSubscriptionByConnectorToken(connectorToken)
+            if (existingSubscription == null) {
+                Log.d(TAG, "Subscription with connectorToken $connectorToken does not exist. Ignoring.")
+                return@launch
+            }
+
+            // Remove subscription
+            Log.d(TAG, "Removing subscription ${existingSubscription.id} with connectorToken $connectorToken")
+            repository.removeSubscription(existingSubscription.id)
+            existingSubscription.upAppId?.let { appId -> distributor.sendUnregistered(appId, connectorToken) }
+
+            // Refresh (and maybe stop) foreground service
+            refreshSubscriberService(app, repository)
+        }
+    }
+
+    private fun refreshSubscriberService(context: Context, repository: Repository) {
+        Log.d(TAG, "Refreshing subscriber service")
+        val subscriptionIdsWithInstantStatus = repository.getSubscriptionIdsWithInstantStatus()
+        val subscriberManager = SubscriberManager(context)
+        subscriberManager.refreshService(subscriptionIdsWithInstantStatus)
     }
 
     companion object {
         private const val TAG = "NtfyUpBroadcastRecv"
+        private const val UP_PREFIX = "up"
         private const val TOPIC_LENGTH = 16
     }
 }
diff --git a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt
index 9bdaf7e..a4c1ad9 100644
--- a/app/src/main/java/io/heckel/ntfy/up/Distributor.kt
+++ b/app/src/main/java/io/heckel/ntfy/up/Distributor.kt
@@ -2,9 +2,11 @@ package io.heckel.ntfy.up
 
 import android.content.Context
 import android.content.Intent
+import android.util.Log
 
 class Distributor(val context: Context) {
     fun sendMessage(app: String, connectorToken: String, message: String) {
+        Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): $message")
         val broadcastIntent = Intent()
         broadcastIntent.`package` = app
         broadcastIntent.action = ACTION_MESSAGE
@@ -14,6 +16,7 @@ class Distributor(val context: Context) {
     }
 
     fun sendEndpoint(app: String, connectorToken: String, endpoint: String) {
+        Log.d(TAG, "Sending NEW_ENDPOINT to $app (token=$connectorToken): $endpoint")
         val broadcastIntent = Intent()
         broadcastIntent.`package` = app
         broadcastIntent.action = ACTION_NEW_ENDPOINT
@@ -23,6 +26,7 @@ class Distributor(val context: Context) {
     }
 
     fun sendUnregistered(app: String, connectorToken: String) {
+        Log.d(TAG, "Sending UNREGISTERED to $app (token=$connectorToken)")
         val broadcastIntent = Intent()
         broadcastIntent.`package` = app
         broadcastIntent.action = ACTION_UNREGISTERED
@@ -31,10 +35,15 @@ class Distributor(val context: Context) {
     }
 
     fun sendRegistrationRefused(app: String, connectorToken: String) {
+        Log.d(TAG, "Sending REGISTRATION_REFUSED to $app (token=$connectorToken)")
         val broadcastIntent = Intent()
         broadcastIntent.`package` = app
         broadcastIntent.action = ACTION_REGISTRATION_REFUSED
         broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
         context.sendBroadcast(broadcastIntent)
     }
+
+    companion object {
+        private const val TAG = "NtfyUpDistributor"
+    }
 }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5fe54e6..f3fbb2e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -47,6 +47,7 @@
     <string name="main_item_status_text_one">%1$d notification</string>
     <string name="main_item_status_text_not_one">%1$d notifications</string>
     <string name="main_item_status_reconnecting">reconnecting …</string>
+    <string name="main_item_status_unified_push">%1$s (UnifiedPush)</string>
     <string name="main_item_date_yesterday">Yesterday</string>
     <string name="main_add_button_description">Add subscription</string>
     <string name="main_no_subscriptions_text">It looks like you don\'t have any subscriptions yet.</string>
@@ -54,8 +55,8 @@
         Click the button below to create or subscribe to a topic. After that, you can send
         messages via PUT or POST and you\'ll receive notifications on your phone.
     </string>
-    <string name="main_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.
-    </string>
+    <string name="main_how_to_link">For more detailed instructions, check out the ntfy.sh website and documentation.</string>
+    <string name="main_unified_push_toast">Subscription is managed by %1$s via UnifiedPush</string>
 
     <!-- Add dialog -->
     <string name="add_dialog_title">Subscribe to topic</string>