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>